Java8总结之Stream API

Stream 流 是java 8 中处理集合的关键抽象概念。它可以指定你希望对集合进行的操作。我们使用 Java 8 尽量从以往迭代器转为使用Stream操作。它与集合的区别如下:

  • Stream 自己不会存储元素。元素可能被存储在底层的集合中,或者根据需要产生出来
  • Stream 操作符不会改变源对象。相反,它们会返回一个持有结果的新Stream
  • Stream 操作符可能是延迟执行的。这意味着它们会等到需要结果的时候才执行。如下面的一个例子,可以看出第6行 如果非延迟执行,第一个值是不可能输出的。
    @Test
    public void test4() {
        //生成一个无限的序列,forEach会停不下来。
        Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE)).forEach(System.out::println);
        //可以输出第一个值
        Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE)).findFirst();
    }
  • Stream 的可读性要更好一些。而且可以很容易进行并行执行。如long count = words.parallelStream().filter(w -> w.length() > 12).count(); 只需要Stream修改为parallelStream.

当使用Stream 时,你会通过三个阶段来建立一个操作流水线:
- 创建一个Stream。
- 在一个或者多个步骤中,将初始的Stream转为另一个Stream的中间操作。
- 使用一个终止操作来产生一个结果。该操作会强制它之前的延迟操作立即执行。这和Spark中的终止操作很像。
下面我们来一步步看怎么进行操作

创建Stream

创建Stream一般有以下几种方式
- Collection接口中新添加的stream 方法,可以将任何集合转为一个Stream。如:Arrays.asList("sb","e").stream()
- 如果是一个数组,可以通过Stream的静态方法Stream.of方法将它转为一个Stream 。如:Stream<String> words = Stream.of(contexts.split(","));
- of 方法可以接受可变的参数,因此你可以构造一个含有任意个参数的Stream Stream<String> s = Stream.of("a","b","c");
- 使用Arrays.stream(array,from,to)方法将数组的一部分转为Stream。
- 创建一个空Stream。Stream.empty();
- 创建无限Stream。有人说创建一个无限Stream有啥用,前面说过,创建无限时是不会执行的,只有在终止操作时才会执行,在终止操作前可以进行一些限制从而创建出一些符合条件的Stream。如下

    @Test
    public void test5() {
        List<String>  list =    Arrays.stream("a,b,c,d,e,f".split(",")).collect(Collectors.toList());
        System.err.println(JSON.toJSONString(list));
        int size = list.size();
        List<String> result =   IntStream.iterate(size-1, n -> n-1).limit(size).mapToObj(list::get).collect(Collectors.toList());
        System.err.println(JSON.toJSONString(result));
    }

看出来这个例子是做什么的吗?其实第6行就是将list进行反转。输出结果如下:

["a","b","c","d","e","f"]
["f","e","d","c","b","a"]

IntStream.iterate就是会产生一个 该方法第一个参数是一个初始值,后面的一个函数是对第一个初始值,进行应用该函数,会一直执行下去,但这个动作是延迟的,而类似limit之类的操作保证了只执行次数。
- 还有Files.lines会返回一个包含文件中所有行的Stream。Stream接口有一个父接口AutoCloseable。当在某个Stream上调用close 方法时,底层的文件也会被关闭。
- 还有Pattern.complie("[\\P{L}]+").splitAsStream(contexts) 可以按正则表达式对对象进行分隔。
- 其它方法。

转换Stream

转换Stream其实就是把一个Stream通过某些行为转换成一个新的Stream。Stream这种操作很多,只能选择几个常用的进行说一下。

  • filter: 对于Stream中包含的元素使用给定的过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素;
    Stream<T> filter(Predicate<? super T> predicate) filter接受一个Predicate (断言)函数式接口
    Predicate 中有三个默认方法一个default(and, negate,or)法,一个默认方法(isEqual),一个抽象方法(test).
    具体分析可查看Interface Predicate 中的介绍,测试代码如下:
  Predicate<String> a = x -> x != null;
  Predicate<String> b = a.and(x -> !"".equals(x));
  Stream.of("a","","b",null).filter(b.negate()).forEach(System.out::println);

filter方法示意图:
enter image description here
- map: 对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。这个方法有三个对于原始类型的变种方法,分别是:mapToInt,mapToLong和mapToDouble。这三个方法也比较好理解,比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的Stream中的元素都是int类型。之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗;
<R> Stream<R> map(Function<? super T,? extends R> mapper) map接受一个Function函数接口。表示接受一个参数并产生结果的函数。
它有二个 default方法(andThen, compose)一个静态方法(identity)

   Function<Integer, Integer> f = x -> x * 3;
   System.err.println(f.andThen(x -> x + 4).apply(6)); //结果 22 先执行6*3 再进行 +4操作 6*3+4
   System.err.println(f.compose(x -> Integer.valueOf(x.toString()) + 4).apply(6)); //结果为30:
   先将x+4执行*3操作(x+4)*3=3x+12 再将6执行上面的操作 3*6+12=30 

map方法示意图:
enter image description here
- flatMap:和map类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合中;

flatMap方法示意图:
enter image description here

     String[] words = new String[]{"Hello","World"};
        List<String[]> a = Arrays.stream(words)
                .map(word -> word.split(""))
                .distinct()
                .collect(toList());
        a.forEach(System.out::print);

enter image description here

        String[] words = new String[]{"Hello","World"};
        List<String> a = Arrays.stream(words)
                .map(word -> word.split(""))
                .flatMap(Arrays::stream)
                .distinct()
                .collect(toList());
        a.forEach(System.out::print);

使用flatMap方法的效果是,各个数组并不是分别映射一个流,而是映射成流的内容,所有使用map(Array::stream)时生成的单个流被合并起来,即扁平化为一个流。
enter image description here

  • peek: 生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数;

peek方法示意图:
enter image description here

  • limit: 对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素;

limit方法示意图:
enter image description here

  • skip: 返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream;

skip方法示意图:
enter image description here

  • distinct: 对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素;
    distinct方法示意图
    enter image description here

  • sorted: 遍历整个流,并在产生任何元素之前对它进行排序。

 Arrays.stream("a,w,d,e,f,b,c,j,h".split(",")).sorted().forEach(System.out::println);        Arrays.stream("a,we,deee,eff,fw,b,c,j,h".split(",")).sorted(Comparator.comparing(String::length).reversed()).forEach(System.out::println);
  • 连续多次操作
List<Integer> nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
System.out.println(“sum is:”+nums.stream().filter(num -> num != null).
            distinct().mapToInt(num -> num * 2).
            peek(System.out::println).skip(2).limit(4).sum());

这段代码演示了上面介绍的所有转换方法(除了flatMap),简单解释一下这段代码的含义:给定一个Integer类型的List,获取其对应的Stream对象,然后进行过滤掉null,再去重,再每个元素乘以2,再每个元素被消费的时候打印自身,在跳过前两个元素,最后去前四个元素进行加和运算(解释一大堆,很像废话,因为基本看了方法名就知道要做什么了。这个就是声明式编程的一大好处!)。大家可以参考上面对于每个方法的解释,看看最终的输出是什么。
- 性能问题
有些细心的同学可能会有这样的疑问:在对于一个Stream进行多次转换操作,每次都对Stream的每个元素进行转换,而且是执行多次,这样时间复杂度就是一个for循环里把所有操作都做掉的N(转换的次数)倍啊。其实不是这样的,转换操作都是lazy的,多个转换操作只会在汇聚操作(见下节)的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在汇聚操作的时候循环Stream对应的集合,然后对每个元素执行所有的函数。

聚合操作(Reduce)

  • reduce:这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。例如 Stream 的 sum 就相当于
Integer sum = integers.reduce(0, (a, b) -> a+b); //或
Integer sum = integers.reduce(0, Integer::sum);

也有没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。
- collect :该方法功能比较强大,能将流收集成很多形式。collect方法接收的多个参数中主要有一个Collector接口,该接口的实现类Collectors提供很多静态方法便于多种形式的收集,比较强大。

1. 归约

1.1.计数
  long count = list.stream()  .collect(Collectors.counting());  
  long count = list.stream().count(); //两个相同
1.2. 最值
Optional<Person> oldPerson = list.stream().collect(Collectors.maxBy(Comparator.comparingInt(Person::getAge)));
1.3.求平均值
double avg = list.stream().collect(Collectors.averagingInt(Person::getAge));
1.4.求和
int summing = list.stream().collect(Collectors.summingInt(Person::getAge));
1.5.连接字符串
String names = list.stream().collect(Collectors.joining());
String names = list.stream().collect(Collectors.joining(", "));
//每个字符串默认分隔符为空格,若需要指定分隔符,则在joining中加入参数即可:
1.6.一般性的归约操作

若你需要自定义一个归约操作,那么需要使用Collectors.reducing函数,该函数接收三个参数:
- 第一个参数为归约的初始值
- 第二个参数为归约操作进行的字段
- 第三个参数为归约操作的过程
如求合

Optional<Integer> sumAge = list.stream().collect(Collectors.reducing(0,Person::getAge,(i,j)->i+j));
//相比于 1.4更一般性一些                                   

上面例子中,reducing函数一共接收了三个参数:
- 第一个参数表示归约的初始值。我们需要累加,因此初始值为0
- 第二个参数表示需要进行归约操作的字段。这里我们对Person对象的age字段进行累加。
- 第三个参数表示归约的过程。这个参数接收一个Lambda表达式,而且这个Lambda表达式一定拥有两个参数,分别表示当前相邻的两个元素。由于我们需要累加,因此我们只需将相邻的两个元素加起来即可。
Collectors.reducing方法还提供了一个单参数的重载形式。
你只需传一个归约的操作过程给该方法即可(即第三个参数),其他两个参数均使用默认值。
- 第一个参数默认为流的第一个元素
- 第二个参数默认为流的元素
求合。

Optional<Integer> sumAge = list.stream().collect(Collectors.reducing((i,j)->i+j));

2.分组

分组就是将流中的元素按照指定类别进行划分,类似于SQL语句中的GROUPBY。

2.1.一级分组

例:将所有人分为老年人、中年人、青年人

Map<String,List<Person>> result = list.stream()
                                    .collect(Collectors.groupingby((person)->{
        if(person.getAge()>60)
            return "老年人";
        else if(person.getAge()>40)
            return "中年人";
        else
            return "青年人";
                                    }));

groupingby函数接收一个Lambda表达式,该表达式返回String类型的字符串,groupingby会将当前流中的元素按照Lambda返回的字符串进行分组。
分组结果是一个Map< String,List< Person>>,Map的键就是组名,Map的值就是该组的Perosn集合。

2.2.多级分组

多级分组可以支持在完成一次分组后,分别对每个小组再进行分组。
使用具有两个参数的groupingby重载方法即可实现多级分组。
- 第一个参数:一级分组的条件
- 第二个参数:一个新的groupingby函数,该函数包含二级分组的条件

例:将所有人分为老年人、中年人、青年人,并且将每个小组再分成:男女两组。

Map<String,Map<String,List<Person>>> result = list.stream()
                                    .collect(Collectors.groupingby((person)->{
        if(person.getAge()>60)
            return "老年人";
        else if(person.getAge()>40)
            return "中年人";
        else
            return "青年人";},groupingby(Person::getSex)));

此时会返回一个非常复杂的结果:Map< String,Map< String,List< Person>>>。

2.3.对分组进行统计

拥有两个参数的groupingby函数不仅仅能够实现多几分组,还能对分组的结果进行统计。

例:统计每一组的人数

Map<String,Long> result = list.stream()
                                    .collect(Collectors.groupingby((person)->{
        if(person.getAge()>60)
            return "老年人";
        else if(person.getAge()>40)
            return "中年人";
        else
            return "青年人";
                                    },
                                    counting()));

此时会返回一个Map< String,Long>类型的map,该map的键为组名,map的值为该组的元素个数。

3.分片

参考:
书:《写给大忙人看的Java SE 8》
并发编程网
jdk-1.8-google

猜你喜欢

转载自blog.csdn.net/lhn1234321/article/details/81351001