文章目录

  • Pre
  • 多级分组
  • 按子组收集数据
    • 查找每个子组中热量最高的 Dish
    • 图解工作过程
    • 与 groupingBy联合使用的其他收集器的例子


Pre

来看个小例子: 把菜单中的菜按照类型进行分类,有菜的放一组,有肉的放一组,其他的都放另一组。

Map<Dish.Type, List<Dish>> collect = menu.stream().collect(groupingBy(Dish::getType));

用 Collectors.groupingBy 工厂方法返回的收集器就可以轻松地完成这项任务。

这里,给 groupingBy 方法传递了一个 Function (以方法引用的形式),它提取了流中每一道 Dish 的 Dish.Type 。我们把这个 Function 叫作分类函数,因为它用来把流中的元素分成不同的组。

如下图所示,分组操作的结果是一个 Map ,把分组函数返回的值作为映射的键,把流中所有具有这个分类值的项目的列表作为对应的映射值。

在菜单分类的例子中,键就是菜的类型,值就是包含所有对应类型的菜的列表。


【第二个例子】

但是,分类函数不一定像方法引用那样可用,因为你想用以分类的条件可能比简单的属性访问器要复杂。

例如,你可能想把热量不到400卡路里的菜分为“低热量”(diet),热量400到700卡路里的菜为“普通”(normal),高于700卡路里的菜为“高热量”(fat)。

由于 Dish 类 没有把这个操作写成一个方法,你无法使用方法引用,但你可以把这个逻辑写成Lambda表达式:

    public enum CaloricLevel { DIET, NORMAL, FAT }Map<CaloricLevel, List<Dish>> collect = menu.stream().collect(groupingBy(dish -> {if (dish.getCalories() > 300) {return CaloricLevel.DIET;} else if (dish.getCalories() <= 700) {return CaloricLevel.NORMAL;} else {return CaloricLevel.FAT;}}));

多级分组

现在,已经看到了如何对菜单中的菜肴按照类型和热量进行分组,但要是想同时按照这两个标准分类怎么办呢?分组的强大之处就在于它可以有效地组合。让我们来看看怎么做。

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

 public static Map<Dish.Type, Map<CaloricLevel, List<Dish>>> duobleGroup(){Map<Dish.Type, Map<CaloricLevel, List<Dish>>> collect = menu.stream().collect(groupingBy(Dish::getType, groupingBy(dish -> {if (dish.getCalories() <= 400) {return CaloricLevel.DIET;} else if (dish.getCalories() <= 700) {return CaloricLevel.NORMAL;} else {return CaloricLevel.FAT;}})));System.out.println(collect);return collect;}

我们把Dish的toString方法改写一下

    @Overridepublic String toString() {return name;}

输出

{MEAT={FAT=[pork], DIET=[chicken], NORMAL=[beef]}, OTHER={DIET=[rice, season fruit], NORMAL=[french fries, pizza]}, FISH={DIET=[prawns], NORMAL=[salmon]}}

输出的结果里的外层 Map 的键就是第一级分类函数生成的值:“fish, meat, other”, 而这个 Map 的值又是一个 Map ,键是二级分类函数生成的值:“normal, diet, fat”。最后,第二级 map 的值是流中元素构成的 List ,是分别应用第一级和第二级分类函数所得到的对应第一级和第二级键的值:“salmon、pizza…” 这种多级分组操作可以扩展至任意层级,n级分组就会得到一个代表n级树形结构的n级Map 。

一般来说,把 groupingBy 看作“桶”比较容易明白。第一个 groupingBy 给每个键建立了一个桶。然后再用下游的收集器去收集每个桶中的元素,以此得到n级分组。


按子组收集数据

上个例子中,我们看到可以把第二个 groupingBy 收集器传递给外层收集器来实现多级分组。但进一步说,传递给第一个 groupingBy 的第二个收集器可以是任何类型,而不一定是另一个 groupingBy 。

例如,要数一数菜单中每类菜有多少个,可以传递 counting 收集器作为groupingBy 收集器的第二个参数

menu.stream().collect(groupingBy(Dish::getType, counting()))

输出

{FISH=2, MEAT=3, OTHER=4}

还要注意,普通的单参数 groupingBy(f) (其中 f 是分类函数)实际上是 groupingBy(f,toList()) 的简便写法。

再举一个例子,你可以把前面用于查找菜单中热量最高的菜的收集器改一改,按照菜的类型分类:

 // 按类型分类System.out.println(menu.stream().collect(groupingBy(Dish::getType)));// 按类型分类,获取最高热量的菜System.out.println(menu.stream().collect(groupingBy(Dish::getType, maxBy(Comparator.comparing(Dish::getCalories)))));

输出

{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza], MEAT=[pork, beef, chicken]}
{FISH=Optional[salmon], OTHER=Optional[pizza], MEAT=Optional[pork]}

这个 Map 中的值是 Optional ,因为这是 maxBy 工厂方法生成的收集器的类型,但实际上,如果菜单中没有某一类型的 Dish ,这个类型就不会对应一个 Optional. empty() 值,而且根本不会出现在 Map 的?中。 groupingBy 收集器只有在应用分组条件后,第一次在流中找到某个键对应的元素时才会把键加入到分组 Map 中。这意味着 Optional 包装器在这里不是很有用,因为它不会仅仅因为它是归约收集器的返回类型而表达一个最终可能不存在却意外存在的值。

【把收集器的结果转换为另一类型】

因为分组操作的 Map 结果中的每个值上包装的 Optional 没什么用,所以你可能想要把它们去掉。要做到这一点,或者更一般地来说,把收集器返回的结果转换为另一种类型,你可以使用Collectors.collectingAndThen 工厂方法返回的收集器

查找每个子组中热量最高的 Dish

// 按类型分类,获取最高热量的菜System.out.println(menu.stream().collect(groupingBy(Dish::getType,collectingAndThen(maxBy(Comparator.comparing(Dish::getCalories)),Optional::get))));

输出

{FISH=salmon, OTHER=pizza, MEAT=pork}

这个工厂方法接受两个参数——要转换的收集器以及转换函数,并返回另一个收集器。这个收集器相当于旧收集器的一个包装, collect 操作的最后一步就是将返回值用转换函数做一个映射。在这里,被包起来的收集器就是用 maxBy 建立的那个,而转换函数 Optional::get 则把返回的 Optional 中的值提取出来。

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


图解工作过程

从最外层开始逐层向里,注意以下几点

  • 收集器用虚线表示,因此 groupingBy 是最外层,根据菜肴的类型把菜单流分组,得到三个子流
  • groupingBy 收集器包裹着 collectingAndThen 收集器,因此分组操作得到的每个子流都用这第二个收集器做进一步归约
  • collectingAndThen 收集器又包裹着第三个收集器 maxBy
  • 随后由归约收集器进行子流的归约操作,然后包含它的 collectingAndThen 收集器会对其结果应用 Optional:get 转换函数。
  • 对三个子流分别执行这一过程并转换而得到的三个值,也就是各个类型中热量最高的Dish ,将成为 groupingBy 收集器返回的 Map 中与各个分类键( Dish 的类型)相关联的值。

与 groupingBy联合使用的其他收集器的例子

一般来说,通过 groupingBy 工厂方法的第二个参数传递的收集器将会对分到同一组中的所有流元素执行进一步归约操作。

例如,你还重用求出所有菜肴热量总和的收集器,不过这次是对每一组 Dish 求和

menu.stream().collect(groupingBy(Dish::getType,summingInt(Dish::getCalories)));

返回

{MEAT=1900, FISH=750, OTHER=1550}

然而常常和 groupingBy 联合使用的另一个收集器是 mapping 方法生成的。这个方法接受两个参数:

  • 一个函数对流中的元素做变换
  • 另一个则将变换的结果对象收集起来

其目的是在累加之前对每个输入元素应用一个映射函数,这样就可以让接受特定类型元素的收集器适应不同类型的对象。

比方说你想要知道,对于每种类型的 Dish 菜单中都有哪些 CaloricLevel 。我们可以把 groupingBy 和 mapping 收集器结合起来,如下所示:

    Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =menu.stream().collect(groupingBy(Dish::getType, mapping(dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET;else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT; },toSet() )));System.out.println(caloricLevelsByType);

输出:

{MEAT=[NORMAL, FAT, DIET], OTHER=[NORMAL, DIET], FISH=[NORMAL, DIET]}

传递给映?方法的转换函数将 Dish 映射成了它的CaloricLevel :生成的 CaloricLevel 流传递给一个 toSet 收集器,它和 toList 类似,不过是把流中的元素映射到一个 Set 而不是 List 中,以便仅保留各不相同的值。

请注意在上一个示例中,对于返回的 Set 是什么类型并没有任何保证。但通过使用 toCollection ,你就可以有更多的控制。例如,你可以给它传递一个构造函数引用来要求 HashSet :

    Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =menu.stream().collect(groupingBy(Dish::getType, mapping(dish -> { if (dish.getCalories() <= 400) return CaloricLevel.DIET;else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;else return CaloricLevel.FAT; },toCollection(HashSet::new))));System.out.println(caloricLevelsByType);

输出

{FISH=[DIET, NORMAL], OTHER=[DIET, NORMAL], MEAT=[DIET, FAT, NORMAL]}

    public  static List<Dish> menu = Arrays.asList(new Dish("pork", false, 800, Dish.Type.MEAT),new Dish("beef", false, 700, Dish.Type.MEAT),new Dish("chicken", false, 400, Dish.Type.MEAT),new Dish("french fries", true, 530, Dish.Type.OTHER),new Dish("rice", true, 350, Dish.Type.OTHER),new Dish("season fruit", true, 120, Dish.Type.OTHER),new Dish("pizza", true, 550, Dish.Type.OTHER),new Dish("prawns", false, 300, Dish.Type.FISH),new Dish("salmon", false, 450, Dish.Type.FISH));

Java 8 - 收集器Collectors_分组groupingBy相关推荐

  1. Java 8 - 收集器Collectors_归约和汇总

    文章目录 Pre 查找流中的最大值和最小值 需求:想要找出热量最高的菜和热量最低的菜 汇总 需求: 求出菜单列表的总热量 需求: 一次操作求出菜单中元素的个数,并得总和.平均值.最大值和最小值 (su ...

  2. Java 8 - 收集器Collectors_分区partitioningBy

    文章目录 概述 Demo 概述 分区是分组的特殊情况:由一个谓词(返回一个布尔值的函数)作为分类函数,它称分区函数 . 分区函数返回一个布尔值,这意味着得到的分组 Map 的键类型是 Boolean ...

  3. Java 8 - 收集器Collectors_实战

    文章目录 Code Code public class CollectorsAction {public static List<Dish> menu = Arrays.asList(ne ...

  4. 深入JVM虚拟机(四) Java GC收集器

    转载自  深入JVM虚拟机(四) Java GC收集器 1 GC收集器 1.1 Serial串行收集器 串行收集器主要有两个特点:第一,它仅仅使用单线程进行垃圾回收:第二,它独占式的垃圾回收. 在串行 ...

  5. Java 8 - 收集器Collectors

    文章目录 Pre 简介 收集器用作高级归约 预定义收集器 Pre 我们前面学到了,流可以用类似于数据库的操作帮助你处理集合. 它们支持两种类型的操作: 中间操作(如 filter 或 map ) 终端 ...

  6. Java GC收集器配置说明

    根据Java GC收集器具体分类,我们可以看出JVM根据需求不同提供了三种选择:串行收集器.并行收集器.并发收集器. 串行收集器只适用于小数据量的情况,我们主要了解一下并行收集器和并发收集器.默认情况 ...

  7. java虚拟机收集器_Java虚拟机(JVM)垃圾回收器G1收集器 - Break易站

    G1收集器 G1(Garbage-First)是JDK7-u4才推出商用的收集器: 1.特点 (A).并行与并发 能充分利用多CPU.多核环境下的硬件优势: 可以并行来缩短"Stop The ...

  8. java默认收集器_jvm默认垃圾收集器

    jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代) jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel ...

  9. zipkin 自定义采样率_分组,采样和批处理– Java 8中的自定义收集器

    zipkin 自定义采样率 在第一篇文章的后续部分,这一次我们将编写一些更有用的自定义收集器:用于按给定的标准进行分组,采样输入,批量处理以及在固定大小的窗口上滑动. 分组(计数事件,直方图) 假设您 ...

最新文章

  1. 从SQL Server 2000/2005到SQL Server 2008的升级测试
  2. ubuntu中minicom安装和使用
  3. 批处理 批量s扫1433_申报资料 | 批量整理图谱(续)
  4. JAVA_list总结
  5. APT: Package ‘vnc4server‘ has no installation candidate 排查过程及解决方法
  6. easyUI+servlet+mysql项目总结
  7. 设计模式—23种设计模式总览
  8. SQL Sever — 上课笔记【主键、外键、唯一性约束、检查约束】
  9. C#AutoResetEvent和ManualResetEvent的区别
  10. python numpy 下载地址
  11. 软件下载页面php,PHP网页制作软件下载
  12. python二进制显示图片_python 读取二进制 显示图片案例
  13. 多态的表现形式有哪些?
  14. 小米5s Plus安装类原生系统
  15. APICloud:让开发移动应用像拼积木一样简单
  16. 练习题---acmcoder上台阶问题-用Java实现
  17. mysql 10045错误
  18. 51单片机8通道自动温度检测系统仿真+ Proteus仿真
  19. pycharms 如何退出 python shell
  20. Java stream 中 peek() 的合理用法

热门文章

  1. linux:进程之间的通信
  2. C++标准模板库(STL)的概念
  3. 81. Leetcode 21. 合并两个有序链表 (排序)
  4. AssertionError: nn criterions don‘t compute the gradient w.r.t. targets
  5. numpy 笔记 view,copy和numpy的运行速度
  6. 文巾解题 1344. 时钟指针的夹角
  7. 【毕业求职季】-听说你想去大厂看学妹,带你看看腾讯微信产品岗面经(已offer)
  8. vue @click 赋值_vue 手写一个时间选择器
  9. LeetCode-二分查找-69. Sqrt(x)
  10. LeetCode-剑指 Offer 14- I. 剪绳子