Collectors.groupingBy() 是 Java 8 Stream API 提供的基础分组方法,通过分类函数将元素归入 Map,键为分类结果、值为对应元素 List;需防范 null 分类函数或 null 键导致的 NullPointerException,支持自定义 Map 类型与下游收集器实现计数、求平均等聚合,并应预处理空值以保障健壮性。

用 Collectors.groupingBy() 实现基础分组
Java 8 引入的 Stream API 提供了最直接的分组方式:Collectors.groupingBy()。它接受一个分类函数(Function),将集合元素按返回值归类到 Map 中,键是分类结果,值是对应元素的 List。
常见错误是传入 null 分类函数,或分类函数返回 null 导致 NullPointerException;另外,默认分组会保留插入顺序(底层用 LinkedHashMap),但若需自定义 Map 类型(如 TreeMap),必须显式指定下游收集器。
- 对
List按age分组:list.stream().collect(Collectors.groupingBy(p -> p.getAge())) - 按字符串长度分组:
words.stream().collect(Collectors.groupingBy(String::length)) - 指定结果为
TreeMap:groupingBy(keyFunc, TreeMap::new, toList())
分组后聚合:不只是分到 List
默认的 groupingBy() 把同组元素收进 List,但实际中常需要统计数量、求和、取平均值等——这时要用「下游收集器」(downstream collector)。
容易忽略的是:下游收集器的类型决定了最终 Map 的值类型。比如用 counting(),值就是 Long;用 summingInt(),值就是 Integer。一旦类型不匹配(如误用 summingDouble() 但字段是 int),编译会报错。
立即学习“Java免费学习笔记(深入)”;
- 统计每个性别的人数:
groupingBy(Person::getGender, counting()) - 按部门分组并计算平均薪资:
groupingBy(Person::getDept, averagingDouble(Person::getSalary)) - 按状态分组并拼接名称:
groupingBy(Status::getCode, mapping(Status::getName, joining(", ")))
处理空值与异常数据
真实数据里常有 null 字段,而 groupingBy() 默认不允许 null 键(抛 NullPointerException)。不能靠 try-catch 掩盖,得在分类前预处理。
两种主流做法:一是用 Objects.requireNonNullElse() 或三元表达式兜底;二是先过滤掉 null 元素再分组。后者更安全,但会丢失数据;前者需确保兜底值语义合理(比如把 null 归为 "UNKNOWN" 是可接受的业务逻辑)。
- 安全提取非空字段:
groupingBy(p -> p.getCategory() != null ? p.getCategory() : "OTHER") - 提前过滤:
list.stream().filter(Objects::nonNull).filter(p -> p.getCategory() != null).collect(...) - 注意:
Collectors.groupingByConcurrent()不解决空键问题,它只保证并发安全,且仍要求键非 null
性能与内存开销要注意的点
分组操作本质是遍历 + 哈希映射,时间复杂度 O(n),但隐含成本容易被低估:每个分组结果都会新建 List(或其它集合),如果原始集合很大、分组粒度很细(比如按 ID 分组),会导致大量小对象,GC 压力上升。
如果只是计数或求极值,优先用 counting()、maxBy() 等无状态收集器;避免写 groupingBy(key, collectingAndThen(toList(), list -> list.size())) 这种低效写法。另外,流式分组无法复用中间结果——想同时做分组+排序,得在分组后对 Map 的 entrySet 再处理,而不是指望一次流操作全搞定。










