JDK1.8 升级这么久!Stream 流的规约操作有哪些?

1,373 阅读6分钟

前段时间介绍了部分 _Stream_常见接口方法,理解起来并不困难,但_Stream_的用法不止于此,本节我们将仍然以_Stream_为例,介绍流的规约操作。

规约操作又被称作折叠操作,是通过某个连接动作将所有元素汇总成一个汇总结果的过程。元素求和、求最大值或最小值、求出元素总个数、将所有元素转换成一个列表或集合,都属于规约操作。_Stream_类库有两个通用的规约操作reduce()collect(),也有一些为简化书写而设计的专用规约操作,比如sum()max()min()count()等。

最大或最小值这类规约操作很好理解(至少方法语义上是这样),我们着重介绍reduce()collect(),这是比较有魔法的地方。

多面手reduce()

reduce操作可以实现从一组元素中生成一个值,sum()max()min()count()等都是reduce操作,将他们单独设为函数只是因为常用。reduce()的方法定义有三种重写形式:

  • 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()最常用的场景就是从一堆值中生成一个值。用这么复杂的函数去求一个最大或最小值,你是不是觉得设计者有病。其实不然,因为“大”和“小”或者“求和"有时会有不同的语义。

需求:从一组单词中找出最长的单词。这里“大”的含义就是“长”。

上述代码会选出最长的单词_love_,其中_Optional_是(一个)值的容器,使用它可以避免_null_值的麻烦。当然可以使用Stream.max(Comparator<? super T> comparator)方法来达到同等效果,但reduce()自有其存在的理由。

上述代码标号(2)处将i. 字符串映射成长度,ii. 并和当前累加和相加。这显然是两步操作,使用reduce()函数将这两步合二为一,更有助于提升性能。如果想要使用map()sum()组合来达到上述目的,也是可以的。

reduce()擅长的是生成一个值,如果想要从_Stream_生成一个集合或者_Map_等复杂的对象该怎么办呢?终极武器collect()横空出世!

终极武器collect()

不夸张的讲,如果你发现某个功能在_Stream_接口中没找到,十有八九可以通过collect()方法实现。collect()是_Stream_接口方法中最灵活的一个,学会它才算真正入门Java函数式编程。先看几个热身的小例子:

上述代码分别列举了如何将_Stream_转换成_List_、Set_和_Map。虽然代码语义很明确,可是我们仍然会有几个疑问:

  • Function.identity()是干什么的?

  • String::length是什么意思?

  • _Collectors_是个什么东西?

接口的静态方法和默认方法

_Function_是一个接口,那么Function.identity()是什么意思呢?这要从两方面解释:

  • Java 8允许在接口中加入具体方法。接口中的具体方法有两种,_default_方法和_static_方法,identity()就是_Function_接口的一个静态方法。

  • Function.identity()返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t形式的Lambda表达式。

上面的解释是不是让你疑问更多?不要问我为什么接口中可以有具体方法,也不要告诉我你觉得t -> tidentity()方法更直观。我会告诉你接口中的_default_方法是一个无奈之举,在Java 7及之前要想在定义好的接口中加入新的抽象方法是很困难甚至不可能的,因为所有实现了该接口的类都要重新实现。试想在_Collection_接口中加入一个stream()抽象方法会怎样?_default_方法就是用来解决这个尴尬问题的,直接在接口中实现新加入的方法。既然已经引入了_default_方法,为何不再加入_static_方法来避免专门的工具类呢!

方法引用

诸如String::length的语法形式叫做方法引用(method references),这种语法用来替代某些特定形式Lambda表达式。如果Lambda表达式的全部内容就是调用一个已有的方法,那么可以用方法引用来替代Lambda表达式。方法引用可以细分为四类:

收集器

相信前面繁琐的内容已彻底打消了你学习Java函数式编程的热情,不过很遗憾,下面的内容更繁琐。但这不能怪Stream类库,因为要实现的功能本身很复杂。

收集器(Collector)是为Stream.collect()方法量身打造的工具接口(类)。考虑一下将一个_Stream_转换成一个容器(或者_Map_)需要做哪些工作?我们至少需要两样东西:

  • 目标容器是什么?是_ArrayList_还是_HashSet_,或者是个_TreeMap_。

  • 新元素如何添加到容器中?是List.add()还是Map.put()

如果并行的进行规约,还需要告诉_collect()_ 3. 多个部分结果如何合并成一个。

结合以上分析,collect()方法定义为<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner),三个参数依次对应上述三条分析。不过每次调用collect()都要传入这三个参数太麻烦,收集器_Collector就是对这三个参数的简单封装,所以_collect()的另一定义为<R,A> R collect(Collector<? super T,A,R> collector)。Collectors_工具类可通过静态方法生成各种常用的_Collector。举例来说,如果要将_Stream_规约成_List_可以通过如下两种方式实现:

通常情况下我们不需要手动指定_collect()的三个参数,而是调用_collect(Collector<? super T,A,R> collector)_方法,并且参数中的Collector_对象大都是直接通过_Collectors_工具类获得。实际上传入的收集器的行为决定了collect()的行为

使用collect()生成Collection

前面已经提到通过collect()方法将_Stream_转换成容器的方法,这里再汇总一下。将_Stream_转换成_List_或_Set_是比较常见的操作,所以_Collectors_工具已经为我们提供了对应的收集器,通过如下代码即可完成:

上述代码能够满足大部分需求,但由于返回结果是接口类型,我们并不知道类库实际选择的容器类型是什么,有时候我们可能会想要人为指定容器的实际类型,这个需求可通过Collectors.toCollection(Supplier<C> collectionFactory)方法完成。

上述代码(3)处指定规约结果是_ArrayList_,而(4)处指定规约结果为_HashSet_。一切如你所愿。

使用collect()做字符串join

这个肯定是大家喜闻乐见的功能,字符串拼接时使用Collectors.joining()生成的收集器,从此告别_for_循环。Collectors.joining()方法有三种重写形式,分别对应三种不同的拼接方式。无需多言,代码过目难忘。

collect()还可以做更多

除了可以使用_Collectors_工具类已经封装好的收集器,我们还可以自定义收集器,或者直接调用collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)方法,收集任何形式你想要的信息。不过_Collectors_工具类应该能满足我们的绝大部分需求,手动实现之间请先看看文档。后续有机会分享更多_Collectors_操作案例