写点什么

Java 函数式编程 Stream.collect() 为什么这么受欢迎?

用户头像
码农架构
关注
发布于: 2021 年 02 月 07 日

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



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

  • 目标容器是什么?是 ArrayList 还是 HashSet,或者是个 TreeMap。

  • 新元素如何添加到容器中?是 List.add()还是 Map.put()。如果并行的进行规约,还需要告诉 collect()

  • 多个部分结果如何合并成一个。


结合以上分析,collect()方法定义为 R collect(Supplier 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 collectionFactory)方法完成。


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


使用 collect()生成 Map

前面已经说过 Stream 背后依赖于某种数据源,数据源可以是数组、容器等,但不能是 Map。反过来从 Stream 生成 Map 是可以的,但我们要想清楚 Map 的 key 和 value 分别代表什么,根本原因是我们要想清楚要干什么。通常在三种情况下 collect()的结果会是 Map:

  • 使用Collectors.toMap()生成的收集器,用户需要指定如何生成 Map 的 key 和 value。

  • 使用Collectors.partitioningBy()生成的收集器,对元素进行二分区操作时用到。

  • 使用Collectors.groupingBy()生成的收集器,对元素做 group 操作时用到。


情况 1:使用 toMap()生成的收集器,这种情况是最直接的,前面例子中已提到,这是和Collectors.toCollection()并列的方法。如下代码展示将学生列表转换成由<学生,GPA>组成的 Map。非常直观,无需多言。


情况 2:使用partitioningBy()生成的收集器,这种情况适用于将Stream中的元素依据某个二值逻辑(满足条件,或不满足)分成互补相交的两部分,比如男女性别、成绩及格与否等。下列代码展示将学生分成成绩及格或不及格的两部分。


情况 3:使用groupingBy()生成的收集器,这是比较灵活的一种情况。跟 SQL 中的 group by 语句类似,这里的 groupingBy()也是按照某个属性对数据进行分组,属性相同的元素会被对应到 Map 的同一个 key 上。下列代码展示将员工按照部门进行分组:


以上只是分组的最基本用法,有些时候仅仅分组是不够的。在 SQL 中使用 group by 是为了协助其他查询,比如

  • 先将员工按照部门分组

  • 然后统计每个部门员工的人数。


Java 类库设计者也考虑到了这种情况,增强版的groupingBy()能够满足这种需求。增强版的groupingBy()允许我们对元素分组之后再执行某种运算,比如求和、计数、平均值、类型转换等。这种先将元素分组的收集器叫做上游收集器,之后执行其他运算的收集器叫做下游收集器(downstream Collector)。


上面代码的逻辑是不是越看越像 SQL?高度非结构化。还有更狠的,下游收集器还可以包含更下游的收集器,这绝不是为了炫技而增加的把戏,而是实际场景需要。考虑将员工按照部门分组的场景,如果我们想得到每个员工的名字(字符串),而不是一个个 Employee 对象,可通过如下方式做到:


使用 collect()做字符串 join

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


collect()还可以做更多

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


发布于: 2021 年 02 月 07 日阅读数: 35
用户头像

码农架构

关注

公众号:码农架构 2018.03.22 加入

专注于系统架构、高可用、高性能、高并发类技术分享

评论

发布
暂无评论
Java函数式编程Stream.collect()为什么这么受欢迎?