315. Java Stream API - 使用 Collectors.groupingBy() 对流元素分组 —— 创建分组映射和直方图的利器

0 阅读3分钟

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分以下的放到不及格组……’ 这样我们就有了一个分组清单,还可以数一数每组有多少人,甚至把他们的名字拼起来打印。”