315. Java Stream API - 使用 Collectors.groupingBy() 对流元素分组 —— 创建分组映射和直方图的利器
在 Java 的 Stream API 中,Collectors.groupingBy() 是一个非常核心的收集器,用于将流中的元素按照某种规则进行分组,并构建一个 Map。你可以利用它创建直方图(Histogram)、分组统计表、分组连接字符串等,灵活性非常强。
✅ 基本用法:将流元素按分类器函数分组
🧠 什么是分类器?
groupingBy() 的第一个参数是一个分类器(classifier)函数,它定义了如何为每个流元素生成分组的键(key)。这个键可以是任意类型(不能为 null),只要能对元素进行合理的分类。
📌 基本示例:按字符串长度分组
Collection<String> strings = List.of("one", "two", "three", "four", "five", "six",
"seven", "eight", "nine", "ten", "eleven", "twelve");
Map<Integer, List<String>> map = strings.stream()
.collect(Collectors.groupingBy(String::length));
map.forEach((key, value) -> System.out.println(key + " :: " + value));
💡 输出结果:
3 :: [one, two, six, ten]
4 :: [four, five, nine]
5 :: [three, seven, eight]
6 :: [eleven, twelve]
🧩 String::length 就是分类器,它把每个字符串按长度分组。结果是一个 Map<Integer, List<String>>,键为长度,值为对应长度的字符串列表。
✅ 进阶用法一:使用下游收集器 counting() 统计每组元素数量
你可以为 groupingBy() 提供一个下游收集器(downstream collector),用于进一步处理每个分组的值。比如统计每组中元素的个数,就可以使用 Collectors.counting()。
Map<Integer, Long> map = strings.stream()
.collect(Collectors.groupingBy(
String::length,
Collectors.counting()
));
map.forEach((key, value) -> System.out.println(key + " :: " + value));
💡 输出结果:
3 :: 4
4 :: 3
5 :: 3
6 :: 2
📊 这就构成了一个按字符串长度统计数量的直方图(Histogram)!非常适用于词频统计、日志分析等场景。
✅ 进阶用法二:使用下游收集器 joining() 拼接每组字符串
如果你希望每个分组的字符串不是以列表的形式展示,而是以逗号分隔的字符串形式呈现,可以使用 Collectors.joining():
Map<Integer, String> map = strings.stream()
.collect(Collectors.groupingBy(
String::length,
Collectors.joining(", ")
));
map.forEach((key, value) -> System.out.println(key + " :: " + value));
💡 输出结果:
3 :: one, two, six, ten
4 :: four, five, nine
5 :: three, seven, eight
6 :: eleven, twelve
📎 非常适合用于打印清晰的报告或日志输出。
✅ 高阶用法:自定义 Map 类型(第三个参数)
有时候你可能不希望使用默认的 HashMap,而想用 TreeMap(按键排序)或 LinkedHashMap(保持插入顺序)。这时候可以使用 groupingBy() 的第三个重载版本,手动传入一个 Map 工厂(Supplier<Map>):
Map<Integer, List<String>> map = strings.stream()
.collect(Collectors.groupingBy(
String::length,
TreeMap::new, // 指定用 TreeMap
Collectors.toList()
));
map.forEach((key, value) -> System.out.println(key + " :: " + value));
💡 输出结果(按 key 排序):
3 :: [one, two, six, ten]
4 :: [four, five, nine]
5 :: [three, seven, eight]
6 :: [eleven, twelve]
📦 注意:默认 groupingBy() 使用的是 HashMap,如果你对顺序有要求,一定要使用此重载!
🧠 小结回顾
| 用法 | 示例 | 返回类型 |
|---|---|---|
| 基本分组 | .groupingBy(String::length) | Map<Integer, List<String>> |
| 分组计数 | .groupingBy(String::length, counting()) | Map<Integer, Long> |
| 分组连接 | .groupingBy(String::length, joining(", ")) | Map<Integer, String> |
| 自定义Map类型 | .groupingBy(String::length, TreeMap::new, toList()) | TreeMap<Integer, List<String>> |
🚀 实际应用场景
- 📊 构建分类直方图(如词长统计、订单状态分类)
- 📋 按属性分类列表(如按部门分组员工)
- 🧾 按属性汇总信息(如每类产品的总销售额)
如果你希望在讲解中更加生动,还可以举一个现实世界的例子,比如:
“就像我们把学生按成绩分等级一样,
groupingBy()就是在说:‘把所有分数在90分以上的放到优秀组,60分以下的放到不及格组……’ 这样我们就有了一个分组清单,还可以数一数每组有多少人,甚至把他们的名字拼起来打印。”