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

    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,单纯的书面理解是很难明白的,码字看方法才是最好的学习方法。所以我的御姐儿,你还是多码码代码吧。本居士很忙的啊。

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 79,014评论 12 120
  • Java8 in action 没有共享的可变数据,将方法和函数即代码传递给其他方法的能力就是我们平常所说的函数式...
    山猫233阅读 458评论 1 2
  • 3.4 说说相等和内部表示 在Lisp中主要有5种相等断言,因为不是所有的对象被创建的时候都是相等意义上的相等。数...
    AllenChyou阅读 581评论 0 6
  • 这是一篇我关于 GCD 的使用以及学习的总结文章。持续更新。感谢诸多大神在此之前写的各类文章,如果可以,我会尽量把...
    黄穆斌阅读 113评论 0 1
  • 好的节目总会给人带来感动。看朗读者,每一个故事每一个人都让我敬佩。今天更是有一份震惊。 尽管他们不是青春靓丽的年纪...
    zzzoou阅读 24评论 0 0