1. 收集器简介

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

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

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

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

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

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可以计算数值的平均数:

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操作你可以就数出菜单中元素的个数,并得到热量总和、平均值、最大值和最小值:

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方法生成的。这个方法接受两个参数:一个函数对流中的元素做变换,另一个则将变换的结果对象收

《Java 8 in Action》Chapter 6:用流收集数据的更多相关文章

  1. java8中用流收集数据

    用流收集数据 汇总 long howManyDishes = menu.stream().collect(Collectors.counting()); int totalCalories = men ...

  2. Java 8 (5) Stream 流 - 收集数据

    在前面已经使用过collect终端操作了,主要是用来把Stream中的所有元素结合成一个List,在本章中,你会发现collect是一个归约操作,就像reduce一样可以接受各种做法作为参数,将流中的 ...

  3. 《Java 8 in Action》Chapter 4:引入流

    1. 流简介 流是Java API的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现).就现在来说,你可以把它们看成遍历数据集的高级迭代器.此外,流还可以透明地并行 ...

  4. 《Java 8 in Action》Chapter 5:使用流

    流让你从外部迭代转向内部迭代,for循环显示迭代不用再写了,流内部管理对集合数据的迭代.这种处理数据的方式很有用,因为你让Stream API管理如何处理数据.这样Stream API就可以在背后进行 ...

  5. 《Java 8 in Action》Chapter 1:为什么要关心Java 8

    自1998年 JDK 1.0(Java 1.0) 发布以来,Java 已经受到了学生.项目经理和程序员等一大批活跃用户的欢迎.这一语言极富活力,不断被用在大大小小的项目里.从 Java 1.1(199 ...

  6. 《Java 8 in Action》Chapter 9:默认方法

    传统上,Java程序的接口是将相关方法按照约定组合到一起的方式.实现接口的类必须为接口中定义的每个方法提供一个实现,或者从父类中继承它的实现. 但是,一旦类库的设计者需要更新接口,向其中加入新的方法, ...

  7. 《Java 8 in Action》Chapter 10:用Optional取代null

    1965年,英国一位名为Tony Hoare的计算机科学家在设计ALGOL W语言时提出了null引用的想法.ALGOL W是第一批在堆上分配记录的类型语言之一.Hoare选择null引用这种方式,& ...

  8. 《Java 8 in Action》Chapter 11:CompletableFuture:组合式异步编程

    某个网站的数据来自Facebook.Twitter和Google,这就需要网站与互联网上的多个Web服务通信.可是,你并不希望因为等待某些服务的响应,阻塞应用程序的运行,浪费数十亿宝贵的CPU时钟周期 ...

  9. 《Java 8 in Action》Chapter 2:通过行为参数化传递代码

    你将了解行为参数化,这是Java 8非常依赖的一种软件开发模式,也是引入 Lambda表达式的主要原因.行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式.一言以蔽之,它意味 着拿出一个代码 ...

随机推荐

  1. Baozi Leetcode solution 1036: Escape a Large Maze

    Problem Statement In a 1 million by 1 million grid, the coordinates of each grid square are (x, y) w ...

  2. 个人永久性免费-Excel催化剂功能第82波-复制粘贴按源区域大小自动扩展收缩目标区域

    日常工作中,复制粘贴的操作,永远是最高频的操作,没有之一,在最高频的操作上,进行优化,让过程更智能,比一天到晚鼓吹人工智能替换人的骇人听闻的新闻来得更实际.此篇带来一点点的小小的改进,让日后无数的复制 ...

  3. Git储藏和引用日志

    在日常工作中,当要经常停下手头的工作区修复临时的BUG,紧急处理来自同事或者经理的请求,但是又不能将手头的工作进行提交的时候.那么Git储藏功能(stash)就起到作用了. 储藏可以捕获我们的工作区状 ...

  4. C#7.0 新增功能

    连载目录    [已更新最新开发文章,点击查看详细] C# 7.0 向 C# 语言添加了许多新功能 01 out 变量 支持 out 参数的现有语法已在此版本中得到改进. 现在可以在方法调用的参数列表 ...

  5. linux初学者-网络桥接篇

    linux初学者-网络桥接篇 在网络的使用中,有时需要搭建网络桥来实现网络桥接.例如在一台主机上制作一台虚拟机,虚拟机是没有物理网卡的,这时虚拟机数据的发送和接收就需要通过主机上的物理网卡,需要主机的 ...

  6. Pinyin4j简单使用教程

    Pinyin4j是一个流行的Java库,支持中文字符和拼音之间的转换,拼音输出格式可以定制,在项目中经常会遇到需求用户输入汉字后转换为拼音的场景,这时候Pinyin4j就可以派上用场 有自己私服的可以 ...

  7. 接口测试时遇到 java 代码加密请求数据,用 python 的我该怎么办?

    前言 自动化测试应用越来越多了,尤其是接口自动化测试. 在接口测试数据传递方面,很多公司都会选择对请求数据进行加密处理. 而目前为主,大部分公司的产品都是java语言实现的.所以加密处理也是java实 ...

  8. hdoj 4706 Children's Day

    题目意思就是用a-z组成一个N,然后到z后又跳回a,输出宽从3到10的N. #include <stdio.h> #include <string.h> char s[14][ ...

  9. 图解Redis之数据结构篇——压缩列表

    前言     同整数集合一样压缩列表也不是基础数据结构,而是 Redis 自己设计的一种数据存储结构.它有点儿类似数组,通过一片连续的内存空间,来存储数据.不过,它跟数组不同的一点是,它允许存储的数据 ...

  10. 一文搞懂 Prometheus 的直方图

    原文链接:一文搞懂 Prometheus 的直方图 Prometheus 中提供了四种指标类型(参考:Prometheus 的指标类型),其中直方图(Histogram)和摘要(Summary)是最复 ...