美文网首页其他零散知识点java EE 专题
JAVA8函数式编程系列2--流(Stream)

JAVA8函数式编程系列2--流(Stream)

作者: 仁昌居士 | 来源:发表于2017-07-20 22:53 被阅读0次

        Jav8中,在核心类库中引入了新的概念,流(Stream)。流使得程序媛们得以站在更高的抽象层次上对集合进行操作。
        今天,居士将主要介绍Steam类中对应集合上操作的几个重要的方法。

    1、 Steam举例

        对使用Java的程序媛们,当需要处理集合里的每一个数据时,通常是使用迭代,再对每个返回的元素进行处理。比如:

    int count = 0;
      ArrayList<String> nameList = new ArrayList<>();
            nameList.add("仁昌居士");
            nameList.add("仁昌居士");
            nameList.add("痕无羽");
            nameList.add("羽无痕");
            for (String name: nameList) {
                if(name.equals("仁昌居士"))
                   count++;
            }
    

        尽管这段代码思想上 并不难理解,但是存在几个问题:
        (1) 从代码量上来看,每一次的循环集合类,都需要重复写很多的样板代码。
        (2)对于for循环写的代码块,有些程序媛可能很难理解其编写意图。需要阅读整个循环体后,才能有一定的理解。假设只有一个for循环,相对理解并不难,但是当出现多层嵌套循环,那理解所花费的成本就大幅度提升了。
        分析一下for循环的实现原理,可知是通过调用了iterator()方法,产生了一个Iterator对象,通过while方法遍历的显式调用这个对象的hasNext()和next()方法。以实现需求。这种遍历过程叫做外部遍历,是一种串行化操作。

     Iterator<String> iterator = nameList.iterator();
            while(iterator.hasNext()){
                String name = iterator.next();
                if(name.equals("仁昌居士")){
                     count++;
                }
            }
    

        注意事项:为什么对for循环叫他外部遍历而不是外部迭代的原因?可见另一篇文章:还未写,周末写。
        相对于外部遍历,还有一种方法叫做内部遍历。通过内部遍历,将上述代码实现为:

     long count = nameList.stream().filter(name -> name.equals("仁昌居士"))
                    .count();
    

        上述代码实际是三步,第一步:nameList创建了一个Stream实例,第二步:用fliter操作符过滤找出为“仁昌居士”的name,并转换成另外一个Stream,第三步:把Stream的里面包含的内容按照某种算法来成型成一个值,代码中式用count操作符计算有几个这样的name。

    2、 惰性求值和及早求值

        通常,在Java中调有一个方法,计算机会随机执行相应的操作,比如通过println在终端上输出一条信息。Stream里的方法则有些不同。比如说:

    nameList.stream().filter(name -> name.equals("仁昌居士"));
    

        这行代码并没有通过fliter得到新的集合,只是对Stream进行了描述,这种方法叫做“惰性求值”方法,而之后的“.count()”使Stream产生了值的方法,叫做“及早求值”方法。
        最好的验证方式就如下。
        单纯的在filter中加入一条println语句:

       nameList.stream()
                    .filter(name -> {
                      System.out.println(name);
                        return name.equals("仁昌居士");
                    });
    

         运行结果是程序并没有输出对应信息。
        再测试:在后面加入一个及早求值方法,如count(),将会得到输出结果。

     nameList.stream()
                    .filter(name -> {
                        System.out.println(name);
                        return name.equals("仁昌居士");
                    })
                    .count();
    
    输出结果

        想知道操作符是惰性求值操作符还是及早求值操作符,只需观察其返回值,如果返回值是Stream,则是惰性求值操作符;如果返回值是另一个类型或者是void,则是及早求值操作符。通过这种多个惰性求值操作符+一个及早求值操作符消费为结尾的链来得到想要的值,这个过程和建造者Builder模式很相似。建造者Builder模式就是通过使用一系列操作设置属性和配置,最后通过一个build方法,将对象真正创建出来。

    3、 常用的Stream操作符

        现在讲述几个比较常用的Stream API。

    3.1 创建Stream操作符

    3.1.1 of

        Stream的of操作符,是将一组数据生成一个Stream。是一个惰性求值操作符。

    Stream nameStream =  Stream.of("仁昌居士","痕无羽","羽无痕");
    

    3.1.2 generate

        生成一个无限长度的Stream,其元素的生成是通过给定的Supplier(这个接口可以看成一个对象的工厂,每次调用返回一个给定类型的对象),也是一个惰性求值操作符。

    Stream.generate(() -> Math.random());
    

        生成一个无限长度的Stream,其中值是随机的。这个无限长度Stream是懒加载,一般这种无限长度的Stream都会配合Stream的limit()方法来用。

    3.1.3 iterate

        iterate操作符生成无限长度的Stream,和generator不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的。其中包含的元素可以认为是:seed,f(seed),f(f(seed))无限循环,也是惰性求值操作符

    Stream.iterate(1, item -> item + 1).limit(10).forEach(System.out::println);
    

        这段代码就是先获取一个无限长度的正整数集合的Stream,然后取出前10个打印。千万注意:使用limit方法,不然会无限打印下去。

    3.2 转换Stream操作符

    3.2.1 map

        map操作符的作用就是将Stream中的每个值进行同一个操作的处理后,再将其转换为一个新的Stream,所以是惰性求值操作符。

     List<String> list =  Stream.of(1,2,3)
                    .map(integer -> String.valueOf(integer))
                    .collect(Collectors.toList());
    

        看上面这段代码可知,通过map操作符和Lambda表达式将一个Integer类型的参数转成了一个String的返回值。参数和返回值直接不必是同一种类型,但是Lambda表达式,必须是Function接口(只包含一个参数的普通函数接口)的一个实例。
        注意事项:用map操作符得到的还是Stream。

    3.2.2 flatMap

        flatMap操作符不同于map操作符将Stream中的值转换为新值,他能将多个Stream合成一个Stream,返回值也是Stream。是惰性求值操作符。

            ArrayList<Integer> arrayList1 = new ArrayList<>();
            arrayList1.add(1);
            arrayList1.add(2);
            ArrayList<Integer> arrayList2 = new ArrayList<>();
            arrayList2.add(3);
            arrayList2.add(4);
            List<Integer> list3 = Stream.of(arrayList1,arrayList2)
            .flatMap(numbers -> numbers.stream())
            .collect(Collectors.toList());
    

        通过stream()方法,将每个ArrayList转换成了Stream对象,其余部分由flatMap操作符处理,得到的Stream是Stream.of(1,2,3,4)。

    3.2.3 distinct

        distinct操作符,是对Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素。是惰性求值操作符。

    Stream stream = Stream.of(1, 2, 3, 4,1,2,2,3,4)
                    .distinct();
    

        得到的Stream里面的元素只有1,2,3,4四个。重复的都被去掉了。

    3.2.4 filter

        fliter操作符,上文已经提及过了,是用于过滤的惰性求值操作符。

      List<Integer> list =  Stream.of(1,2,3)
                    .filter(integer -> integer >1)
                    .collect(Collectors.toList());
    

        和map操作符相似,filter操作符接受一个函数为参数,该函数通过Lambda表达式表示,如这段代码,Lambda表达式将会对大于1的返回true,否则返回false。这段代码就是通过filter操作符过滤选择Lambda表达式返回值为true的元素保留生成新的Stream,并通过collect操作符得到符合要求的List。

    3.2.5 peek

        peek生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),当最终用及早求值操作符消费此Stream时,新Stream每个元素都会执行给定的消费函数。是惰性求值操作符。

     nameList.stream()
                    .filter(name -> name.equals("仁昌居士"))
                    .peek(name -> System.out.println(name))
                    .collect(Collectors.toList());
    

    3.2.6 limit

        limit对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素,是惰性求值操作符。

    Stream stream = Stream.of(1, 2, 3, 4,5,6,7,8,9,10)
                    .limit(3);
    

        得到的新的Stream的元素只有前3个。后面的被截断了。

    3.2.7 skip

        返回一个跳过原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream,是惰性求值操作符。

    Stream stream = Stream.of(1, 2, 3, 4,5,6,7,8,9,10)
                    .skip(3);
    

        得到的新的Stream的元素只有后7个。前面的3个被跳过不要了。

    3.3 成型(Reduce)Stream操作符

         成型(Reduce)Stream操作符和.reduce()操作符是两个东西。
        成型(Reduce)是个概念,我将其理解为将Stream在经过多次转换操作后确定最终成型得到一个特定的非Stream的结果。
        而成型(Reduce)Stream操作符就是对Stream反复使用某个合并操作,把序列中的元素合并成一个整合结果的操作符。比如:max操作符、min操作符、sum操作符、count操作符、reduce()操作符、collect操作符等等。
        注意事项:其中collect操作符和其他几个操作符不同。他最终成型的结果是一个可变的容器,比如Collection或者StringBuilder。

    3.3.1 max和min

        Stream中进行大小比较是比较常用的操作,所以有了max和min操作符,返回值类型是Optional,这是Java8防止出现NPE的一种可行方法,后面的文章会详细介绍,这里就简单的认为是一个容器,其中可能会包含0个或者1个对象。。
        查找Stream中的最大或最小元素,就要考虑是用什么作为排序的指标。

       Integer integer3 = Stream.of(1, 2, 3, 4)
                    .min((x,y) -> x.compareTo(y))
                    .get();
    

        通过比较两个对象的值的大小,来得到最小值。对于这个指标,也可以通过Comparator对象。

     Integer integer = Stream.of(1, 2, 3, 4)
                    .min(Comparator.naturalOrder())
                    .get();
    

        max和min方法同理,意思也一目了然,所以不用过多描述,都是及早求值操作符。

    3.3.2 sum

        sum操作符不是所有的Stream对象都有的,只有IntStream、LongStream和DoubleStream是实例才有。

     int sum = IntStream.of(1, 2, 3, 4,5,6,7,8,9,10)
                    .sum ();
    

        sum为55。求和的及早求值操作符。

    3.3.3 count

        count操作符不是求Stream中元素的数量。

    long count= Stream.of(0,1, 2, 3, 4,5,6,7,8,9)
                    .count();
    

        count为10。求元素个数的及早求值操作符。

    3.3.4 reduce

        reduce操作符是及早求值操作符,接受一个元素序列为输入,反复使用某个合并操作,把序列中的元素合并成一个汇总的结果,其生成的值不是随意的,而是根据指定的计算模型。像之前的count、min、max操作符都是reduce操作。
        reduce方法有三个override的方法。

    Optional<T> reduce(BinaryOperator<T> accumulator);
    T reduce(T identity, BinaryOperator<T> accumulator);
    <U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
    

         先来看reduce方法的第一种形式,其方法定义如下:

    Optional<T> reduce(BinaryOperator<T> accumulator);
    

        接受一个BinaryOperator类型的参数,在使用的时候我们可以用lambda表达式来。

    Stream.of(1,2,3,4,5,6,7,8,9,10).reduce((sum, item) -> sum + item).get();
    

        结果都为55。
         可以看到reduce方法接受一个函数,这个函数有两个参数,第一个参数是上次函数执行的返回值(也称为中间结果),第二个参数是stream中的元素,这个函数把这两个值相加,得到的和会被赋值给下次执行这个函数的第一个参数。要注意的是:第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素。这个方法返回值类型是Optional。
        再来看reduce方法的第二种形式,其方法定义如下:

    T reduce(T identity, BinaryOperator<T> accumulator);
    

        与第一种变形相同的是都会接受一个BinaryOperator函数接口,不同的是其会接受一个identity参数,用来指定Stream循环的初始值。如果Stream为空,就直接返回该值。另一方面,该方法不会返回Optional,因为该方法不会出现null。

    Stream.of(1,2,3,4,5,6,7,8,9,10).reduce(1, (sum, item) -> sum + item)).get();
    

        结果都为56。
        变形1,未定义初始值,从而第一次执行的时候第一个参数的值是Stream的第一个元素,第二个参数是Stream的第二个元素。
         变形2,定义了初始值,从而第一次执行的时候第一个参数的值是初始值,第二个参数是Stream的第一个元素。

    <U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
    

        对于第三种变形,我们先看各个参数的含义,第一个参数类型是实际返回实例的数据类型,同时其为一个泛型也就是意味着该变形的可以返回任意类型的数据,第二个参数累加器accumulator,可以使用二元?表达式(即二元lambda表达式),声明你在u上累加你的数据来源t的逻辑,例如(u,t)->u.sum(t),此时lambda表达式的行参列表是返回实例u和遍历的集合元素t,函数体是在u上累加t,第三个参数组合器combiner,同样是二元?表达式,(u,t)->u, 是用来处理并发操作的。因为Stream是支持并发操作的,为了避免竞争,对于reduce线程都会有独立的result,combiner的作用在于合并每个线程的result得到最终结果。这也说明了了第三个函数参数的数据类型必须为返回数据类型了。代码并不好举例,先不距离,在以后的讲解中会提及。

    3.3.5 collect

         collect操作符:是一个及早求值操作符。它可以把Stream中的要有元素收集到一个结果容器中(比如Collection)。先看一下最通用的collect方法的定义(还有其他override方法)。

    <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
    

        先来看看这三个参数的含义:Supplier supplier是一个工厂函数,用来生成一个新的容器;BiConsumer accumulator也是一个函数,用来把Stream中的元素添加到结果容器中;BiConsumer combiner还是一个函数,用来把中间状态的多个结果容器合并成为一个(并发的时候会用到)。

    List<Integer> numsWithoutNull = Stream.of(1,2,3,4,5,6,7,8,9,10)
                        .collect(() -> new ArrayList<Integer>(),(list, item) -> list.add(item),(list1, list2) -> list1.addAll(list2));
    

        上面这段代码就是把一个元素是Integer类型的List收集到一个新的List中。进一步看一下collect方法的三个参数,都是lambda形式的函数。
         第一个函数生成一个新的ArrayList实例;
        第二个函数接受两个参数,第一个是前面生成的ArrayList对象,二个是stream中包含的元素,函数体就是把stream中的元素加入ArrayList对象中。第二个函数被反复调用直到原stream的元素被消费完毕;
        第三个函数也是接受两个参数,这两个都是ArrayList类型的,函数体就是把多个ArrayList容器合并成为一个。
         但是上面的collect方法调用有些复杂了,有更简单的override方法,其依赖Collector

    <R, A> R collect(Collector<? super T, A, R> collector);
    

        进一步,Java8还给我们提供了Collector的工具类–Collectors,其中已经定义了一些静态工厂方法,比如:Collectors.toCollection()收集到Collection中, Collectors.toList()收集到List中和Collectors.toSet()收集到Set中,等等。这样的静态方法还有很多,这里就不一一介绍了,大家可以直接去看文档。下面看看使用Collectors对于代码的简化:

    
    List<Integer> numsWithoutNull = Stream.of(1,2,3,4,5,6,7,8,9,10)
                    .collect(Collectors.toList());
    

        这段代码将of()操作符得到的Stream,用collect(Collectors.toList())操作符从Stream中生成一个List。

    4、 性能问题

        完成了上述的讲解,会发现在使用操作符时,会出现对于一个Stream进行多次转换操作,每次都对Stream的每个元素进行转换,而且是执行多次,这样时间复杂度就是一个for循环里把所有操作都做掉的N(转换的次数)倍啊。其实不是这样的,转换操作都是lazy的,多个转换操作只会在成型(Reduce)操作的时候融合起来,一次循环完成。我们可以这样简单的理解,Stream里有个操作函数的集合,每次转换操作就是把转换函数放入这个集合中,在成型(Reduce)操作的时候循环Stream对应的集合,然后对每个元素一次性执行所有的操作。

    5、总结

        对于Stream,单纯的书面理解是很难明白的,码字看方法才是最好的学习方法。所以我的御姐儿,你还是多码码代码吧。本居士很忙的啊。

    相关文章

      网友评论

        本文标题:JAVA8函数式编程系列2--流(Stream)

        本文链接:https://www.haomeiwen.com/subject/vxnahxtx.html