《Java 8 in Action》Chapter 6:用流收集数据

1. 收集器简介

collect() 接收一个类型为 Collector 的参数,这个参数决定了如何把流中的元素聚合到其它数据结构中。Collectors 类包含了大量经常使用收集器的工厂方法,toList() 和 toSet() 就是其中最多见的两个,除了它们还有不少收集器,用来对数据进行对复杂的转换。算法

指令式代码和函数式对比:安全

要是作多级分组,指令式和函数式之间的区别就会更加明显:因为须要好多层嵌套循环和条件,指令式代码很快就变得更难阅读、更难维护、更难修改。相比之下,函数式版本只要再加上 一个收集器就能够轻松地加强数据结构

预约义收集器,也就是那些能够从Collectors类提供的工厂方法(例如groupingBy)建立的收集器。它们主要提供了三大功能:app

  • 将流元素归约和汇总为一个值
  • 元素分组
  • 元素分区

2. 使用收集器

在须要将流项目重组成集合时,通常会使用收集器(Stream方法collect 的参数)。再宽泛一点来讲,但凡要把流中全部的项目合并成一个结果时就能够用。这个结果能够是任何类型,能够复杂如表明一棵树的多级映射,或是简单如一个整数。分布式

3. 收集器实例

3.1 流中最大值和最小值

Collectors.maxBy和 Collectors.minBy,来计算流中的最大或最小值。这两个收集器接收一个Comparator参数来比较流中的元素。你能够建立一个Comparator来根据所含热量对菜肴进行比较:函数

System.out.println("找出热量最高的食物:");
Optional<Dish> collect = DataUtil.genMenu().stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));
collect.ifPresent(System.out::println);
System.out.println("找出热量最低的食物:");
Optional<Dish> collect1 = DataUtil.genMenu().stream().collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories)));
collect1.ifPresent(System.out::println);复制代码

3.2 汇总求和

Collectors类专门为汇总提供了一个工厂方法:Collectors.summingInt。它可接受一个把对象映射为求和所需int的函数,并返回一个收集器;该收集器在传递给普通的collect方法后即执行咱们须要的汇总操做。举个例子来讲,你能够这样求出菜单列表的总热量:优化

Integer collect = DataUtil.genMenu().stream().collect(Collectors.summingInt(Dish::getCalories));
System.out.println("总热量:" + collect);
Double collect1 = Arrays.asList(0.1, 0.2, 0.3).stream().collect(Collectors.summingDouble(Double::doubleValue));
System.out.println("double和:" + collect1);
Long collect2 = Arrays.asList(1L, 2L, 3L).stream().collect(Collectors.summingLong(Long::longValue));
System.out.println("long和:" + collect2);复制代码

3.3 汇总求平均值

Collectors.averagingInt,averagingLong和averagingDouble能够计算数值的平均数:ui

Double collect = DataUtil.genMenu().stream().collect(Collectors.averagingInt(Dish::getCalories));
System.out.println("平均热量:" + collect);
Double collect1 = Arrays.asList(0.1, 0.2, 0.3).stream().collect(Collectors.averagingDouble(Double::doubleValue));
System.out.println("double 平均值:" + collect1);
Double collect2 = Arrays.asList(1L, 2L, 3L).stream().collect(Collectors.averagingLong(Long::longValue));
System.out.println("long 平均值:" + collect2);复制代码

3.4 汇总合集

你可能想要获得两个或更多这样的结果,并且你但愿只需一次操做就能够完成。在这种状况下,你可使用summarizingInt工厂方法返回的收集器。例如,经过一次summarizing操做你能够就数出菜单中元素的个数,并获得热量总和、平均值、最大值和最小值:spa

IntSummaryStatistics collect = DataUtil.genMenu().stream().collect(Collectors.summarizingInt(Dish::getCalories));
System.out.println("int:" + collect);
DoubleSummaryStatistics collect1 = Arrays.asList(0.1, 0.2, 0.3).stream().collect(Collectors.summarizingDouble(Double::doubleValue));
System.out.println("double:" + collect1);
LongSummaryStatistics collect2 = Arrays.asList(1L, 2L, 3L).stream().collect(Collectors.summarizingLong(Long::longValue));
System.out.println("long:" + collect2);复制代码

3.5 链接字符串

joining工厂方法返回的收集器会把对流中每个对象应用toString方法获得的全部字符串链接成一个字符串。线程

String collect = DataUtil.genMenu().stream().map(Dish::getName).collect(Collectors.joining());复制代码

请注意,joining在内部使用了StringBuilder来把生成的字符串逐个追加起来。幸亏,joining工厂方法有一个重载版本能够接受元素之间的分界符,这样你就能够获得一个都好分隔的名称列表:

String collect1 = DataUtil.genMenu().stream().map(Dish::getName).collect(Collectors.joining(","));复制代码

4. 广义的归约汇总

全部收集器,都是一个能够用reducing工厂方法定义的归约过程的特殊状况而已。Collectors.reducing工厂方法是全部这些特殊状况的通常化。它须要三个参数:

  • 第一个参数是归约操做的起始值,也是流中没有元素时的返回值,因此很显然对于数值和而言0是一个合适的值。
  • 第二个参数就是你在6.2.2节中使用的函数,将菜肴转换成一个表示其所含热量的int。
  • 第三个参数是一个BinaryOperator,将两个项目累积成一个同类型的值。这里它就是对两个int求和。

下面两个是相同的操做:

Optional<Dish> collect = DataUtil.genMenu().stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));
Optional<Dish> mostCalorieDish = menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));复制代码

5. 分组

用Collectors.groupingBy工厂方法返回的收集器就能够轻松地完成任务:

Map<Dish.Type, List<Dish>> collect = DataUtil.genMenu().stream().collect(Collectors.groupingBy(Dish::getType));复制代码

给groupingBy方法传递了一个Function(以方法引用的形式),它提取了流中每 一道Dish的Dish.Type。咱们把这个Function叫做分类函数,由于它用来把流中的元素分红不一样的组。分组操做的结果是一个Map,把分组函数返回的值做为映射的键,把流中全部具备这个分类值的项目的列表做为对应的映射值。

5.1 多级分组

要实现多级分组,咱们可使用一个由双参数版本的Collectors.groupingBy工厂方法建立的收集器,它除了普通的分类函数以外,还能够接受collector类型的第二个参数。那么要进行二级分组的话,咱们能够把一个内层groupingBy传递给外层groupingBy,并定义一个为流中项目分类的二级标准:

Map<Dish.Type, Map<CaloricLevel, List<Dish>>> collect1 = DataUtil.genMenu().stream().collect(
        Collectors.groupingBy(Dish::getType,
                Collectors.groupingBy(dish -> {
                    if (dish.getCalories() <= 400) {
                        return CaloricLevel.DIET;
                    } else if (dish.getCalories() <= 700) {
                        return CaloricLevel.NORMAL;
                    } else return CaloricLevel.FAT;
                }))
);复制代码

5.2 按子组收集数据

传递给第一个groupingBy的第二个收集器能够是任何类型,而不必定是另外一个groupingBy。例如,要数一数菜单中每类菜有多少个,能够传递counting收集器做为groupingBy收集器的第二个参数:

Map<Dish.Type, Long> collect2 = DataUtil.genMenu().stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting()));复制代码

还要注意,普通的单参数groupingBy(f)(其中f是分类函数)其实是groupingBy(f, toList())的简便写法。把收集器返回的结果转换为另外一种类型,你可使用 Collectors.collectingAndThen工厂方法返回的收集器,接受两个参数:要转换的收集器以及转换函数,并返回另外一个收集器。

Map<Dish.Type, Dish> collect3 = DataUtil.genMenu().stream().collect(Collectors.groupingBy(Dish::getType,
        Collectors.collectingAndThen(
                Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)),
                Optional::get
        )));复制代码

这个操做放在这里是安全的,由于reducing收集器永远都不会返回Optional.empty()。

经常和groupingBy联合使用的另外一个收集器是mapping方法生成的。这个方法接受两个参数:一个函数对流中的元素作变换,另外一个则将变换的结果对象收􏰁起来。其目的是在累加以前对每一个输入元素应用一个映射函数,这样就可让接受特定类型元素的收􏰁器适应不一样类型的对象。咱们来看一个使用这个收集器的实际例子。比方说你想要知道,对于每种类型的Dish, 菜单中都有哪些CaloricLevel。

Map<Dish.Type, Set<CaloricLevel>> collect4 = DataUtil.genMenu().stream().collect(Collectors.groupingBy(
        Dish::getType, Collectors.mapping(
                dish -> {
                    if (dish.getCalories() <= 400) {
                        return CaloricLevel.DIET;
                    } else if (dish.getCalories() <= 700) {
                        return CaloricLevel.NORMAL;
                    } else return CaloricLevel.FAT;
                }, Collectors.toSet()
        )
));复制代码

6. 分区

分区是分组的特殊状况:由一个谓词(返回一个布尔值的函数)做为分类函数,它称分类函数。分区函数返回一个布尔值,这意味着获得的分组Map的键类型是Boolean,因而它最多能够 分为两组——true是一组,false是一组。例如,若是想要把菜按照素食和非素食分开:

Map<Boolean, List<Dish>> collect = DataUtil.genMenu().stream().collect(Collectors.partitioningBy(Dish::isVegetarian));
System.out.println(collect.get(true));
partitioningBy 工厂方法有一个重载版本,能够像下面这样传递第二个收集器:
Map<Boolean, Map<Dish.Type, List<Dish>>> collect1 = DataUtil.genMenu().stream().collect(Collectors.partitioningBy(
        Dish::isVegetarian, Collectors.groupingBy(Dish::getType)
));复制代码

分区看做分组一种特殊状况。

7. Collectors类的静态工厂方法

8. 收集器接口

public interface Collector<T, A, R> {
        Supplier<A> supplier();
        BiConsumer<A, T> accumulator();
        Function<A, R> finisher();
        BinaryOperator<A> combiner();
        Set<Characteristics> characteristics();
}复制代码

本列表适用如下定义:

  • T是流中要收集的项目的泛型。
  • A是累加器的类型,累加器是在收集过程当中用于累积部分结果的对象。
  • R是手机操做获得的对象(一般但并不必定是集合)的类型。

8.1 创建新的结果容器:supplier方法

supplier方法必须返回一个结果为空的Supplier,也就是一个无参数函数,在调用时它会建立一个空的累加器实例,供数据收集过程使用。

8.2 将元素添加到结果容器:accumulator方法

accumulator方法会返回执行归约操做的函数。当遍历到流中第n个元素时,这个函数执行时会有两个参数:保存归约结果的累加器(已收集了流中的前n-1个项目),还有第n个元素自己。该函数将返回void,由于累加器是原位更新,即函数的执行改变了它的内部状态以体现遍历的元素的效果。

8.3 对结果容器应用最终转换:finisher方法

在遍历完流后,finisher方法必须返回在累积过程的最后要调用的一个函数,以便将累加器对象转换为整个集合操做的最终结果。顺序归约过程的逻辑步骤:

8.4 合并两个结果容器:combiner方法

四个方法中的最后一个——combiner方法会返回一个供归约操做使用的函数,它定义了对流的各个子部分进行并行处理时,各个子部分归约所得的累加器要如何合并:

  • 原始流会以递归方式拆分为子流,直到定义流是否须要进一步拆分的一个条件为非(若是分布式工做单位过小,并行计算每每比顺序计算要慢,并且要是生成的并行任务比处理器内核数多不少的话就毫无心义了)。
  • 如今,全部的子流均可以并行处理,即对每一个子流应用图6-7所示的顺序归约算法。
  • 最后,使用收集器combiner方法返回的函数,将全部的部分结果两两合并。这时会把原始流每次拆分时获得的子流对应的结果合并起来

8.5 characteristics方法

最后一个方法——characteristics会返回一个不可变的Characteristics集合,它定义了收集器的行为——尤为是关于流是否能够并行归约,以及可使用哪些优化的提示。Characteristics是一个包含三个项目的枚举。

  • UNORDERED——归约结果不受流中项目的遍历和累积顺序的影响。
  • CONCURRENT——accumulator函数能够从多个线程同时调用,且该收集器能够并行归约流。若是收集器没有标为UNORDERED,那它仅在用于无序数据源时才能够并行归约。
  • IDENTITY_FINISH——这代表完成器方法返回的函数是一个恒等函数,能够跳过。这种状况下,累加器对象将会直接用做归约过程的最终结果。这也意味着,将累加器A不加检查地转换为结果R是安全的。

9. 小结

  • collect是一个终端操做,它接受的参数是将流中元素累积到汇总结果的各类方式(称为收集器)。
  • 预约义收集器包括将流元素归约和汇总到一个值,例如计算最小值、最大值或平均值。这些收集器总结在表6-1中。
  • 预约义收集器能够用groupingBy对流中元素进行分组,或用partitioningBy进行分区。
  • 收集器能够高效地复合起来,进行多级分组、分区和归约。
  • 你能够实现Collector接口中定义的方法来开发你本身的收集器。

资源获取

  • 公众号回复 : Java8 便可获取《Java 8 in Action》中英文版!

Tips

  • 欢迎收藏和转发,感谢你的支持!(๑•̀ㅂ•́)و✧
  • 欢迎关注个人公众号:庄里程序猿,读书笔记教程资源第一时间得到!

相关文章
相关标签/搜索