Collectors.groupingBy()是Java 8 Stream API中实现集合分组的核心方法,接收分类函数返回Map;支持下游收集器定制聚合结果,可嵌套实现多级分组,但需注意null值处理与并发安全限制。

用 Collectors.groupingBy() 实现基础分组
Java 8 引入的 Stream API 提供了最常用、最直观的集合分组方式。核心是 Collectors.groupingBy(),它接收一个分类函数(Function),返回 Map。
比如对 List 按年龄分组:
Map> byAge = people.stream() .collect(Collectors.groupingBy(Person::getAge));
注意:键类型必须与分类函数返回值一致;若某对象分类函数返回 null,会抛出 NullPointerException —— 这是常见踩坑点,建议提前判空或用 Collectors.groupingBy(person -> person.getAge() != null ? person.getAge() : -1) 容错。
分组后聚合值不是 List?用下游收集器定制
groupingBy() 默认用 Collectors.toList() 收集元素,但实际常需其他形式:统计数量、求和、取平均、转 Set 等。这时要传入第二个参数 —— 下游收集器(downstream collector)。
立即学习“Java免费学习笔记(深入)”;
- 统计每组人数:
Collectors.groupingBy(Person::getGender, Collectors.counting())→Map - 按城市分组并求平均年龄:
Collectors.groupingBy(Person::getCity, Collectors.averagingInt(Person::getAge)) - 去重分组(避免同名重复):
Collectors.groupingBy(Person::getDepartment, Collectors.toSet())
下游收集器决定了最终 value 的类型和语义,选错会导致编译失败或逻辑错误 —— 比如误用 toSet() 却没重写 equals/hashCode,导致分组结果异常。
多级分组:嵌套 groupingBy 或用 groupingByConcurrent?
两层分组(如先按部门、再按职级)可嵌套调用:
Map>> grouped = people.stream() .collect(Collectors.groupingBy(Person::getDept, Collectors.groupingBy(Person::getLevel)));
这种写法清晰,但注意:内层 map 默认是 HashMap,不保证顺序;如需有序,可显式指定 TreeMap::new 作为 mapFactory 参数。
并发场景下,别直接用 groupingByConcurrent() 替换 —— 它只适用于 key 不冲突、且下游收集器线程安全的情况(如 counting()),对 toList() 这类非线程安全收集器,仍可能出错。真正高并发 + 复杂下游时,应考虑分段处理或 parallelStream() 配合手动同步。
原始集合含 null 元素或分组字段为 null 怎么办
groupingBy 对 null 值零容忍:源集合含 null 会触发 NullPointerException;分类函数返回 null 同样崩溃。
稳妥做法是预过滤或映射:
- 过滤掉空对象:
people.stream().filter(Objects::nonNull).collect(...) - 对字段为空做兜底:
Collectors.groupingBy(p -> p.getCategory() != null ? p.getCategory() : "UNKNOWN") - 用
Collectors.groupingBy(keyMapper, LinkedHashMap::new, downstream)保持插入顺序,同时规避null键问题(因LinkedHashMap本身允许null键,但groupingBy仍会拒绝 —— 所以关键还是前端处理)
最容易被忽略的是:数据库查出的实体中,某些字段为 NULL,映射到 Java 对象后变成 null,而业务代码没做任何判空,直接进分组流 —— 这类 NPE 在测试环境常被遗漏,上线后突发。










