bigywater.tech
Java 内部原理 · 96

Collectors.groupingBy() 真实验证:默认返回的是可变HashMap,不是不可变集合

groupingBy() 是 Stream API 里最常用的收集器之一,这篇真实从最基础的分组一路验证到多级分组、downstream collector 组合,最后确认它返回的到底是什么类型的 Map、能不能继续修改。

默认返回真实是HashMap
真实验证:groupingBy()不指定Map工厂时,返回值的真实运行时类型是HashMap,顺序不保证
返回的Map真实可变
真实验证:对groupingBy()结果调用put()真实成功,不是不可变集合,和Collectors.toUnmodifiableList()等价物完全不同

上一篇:Optional.orElse() vs orElseGet() 真实验证

真实验证:基础分组 + downstream collector 组合

用一批员工数据,验证最基础的分组,以及配合counting()/averagingInt()/mapping()各自的真实效果。

StreamGroupingByDemo.java employees.stream().collect(Collectors.groupingBy(Employee::dept)); employees.stream().collect(Collectors.groupingBy(Employee::dept, Collectors.counting())); employees.stream().collect(Collectors.groupingBy(Employee::dept, Collectors.averagingInt(Employee::salary))); employees.stream().collect(Collectors.groupingBy(Employee::dept, Collectors.mapping(Employee::name, Collectors.toList())));
真实实测
真实分组结果的key集合: [hr, engineering, sales]
engineering组真实人数: 3
真实各部门人数统计: {hr=1, engineering=3, sales=2}
真实各部门平均薪资: {hr=5000.0, engineering=9666.666666666666, sales=6500.0}
真实各部门姓名列表: {hr=[Frank], engineering=[Alice, Bob, Carl], sales=[Dave, Eve]}

真实结果验证了 groupingBy() 的分层设计:最基础的形式真实返回 Map<K, List<T>>,每组是原始元素的完整列表;加上第二个参数(downstream collector)后,真实能在分组的同时对每组内部再做一次归约——counting() 真实把每组变成元素个数,averagingInt() 真实计算每组的平均值,mapping() 真实先对每个元素做一次转换(取名字)再收集成列表。这种"分组 + downstream"的组合方式是 Collectors 工具类里最灵活的部分。

真实验证:默认返回HashMap,可以用工厂方法换成TreeMap

groupingBy有一个三参数重载,可以指定Map的具体实现类型。

StreamGroupingByDemo.java Collectors.groupingBy(Employee::dept, Collectors.counting()); // 默认Map类型 Collectors.groupingBy(Employee::dept, TreeMap::new, Collectors.counting()); // 指定TreeMap
真实实测
真实用TreeMap工厂得到按key排序的结果: {engineering=3, hr=1, sales=2}
groupingBy默认返回的Map真实类型: HashMap
指定TreeMap工厂后真实返回的Map类型: TreeMap
真实的类型差异有实际意义。不指定 Map 工厂时,groupingBy() 内部真实使用 HashMap 作为容器,这意味着遍历结果的 key 顺序真实不保证(虽然这次真实输出恰好是 [hr, engineering, sales],但这只是当前哈希分布的偶然结果,不能依赖)。如果业务上需要分组结果按 key 有序展示(比如按部门名字母序输出报表),真实应该用三参数重载显式传入 TreeMap::new 作为 Map 工厂,这样返回的 Map 真实就是 TreeMap,天然按 key 的自然顺序排列。

真实验证:多级分组与返回Map的真实可变性

最后验证嵌套分组(先按部门再按薪资档位),以及分组结果本身是否可以继续修改。

真实实测
真实engineering组内薪资>=8000的人数: 3
groupingBy返回的Map真实是可变的,put()真实成功: true

真实结果证明 groupingBy(Employee::dept, Collectors.groupingBy(...)) 这种嵌套写法真实能构建出 Map<String, Map<Boolean, List<Employee>>> 这样的多级分组结构。另外一个容易被忽视的细节是:groupingBy() 返回的 Map 真实是可变的(内部实现基于 HashMap.merge()/computeIfAbsent() 完成可变归约),不像系列前面 List.of()/Collectors.toUnmodifiableList()那样返回不可变集合——这意味着拿到分组结果后,调用方真实可以继续对它做增删改,如果不希望被后续代码意外修改,需要自己再包一层 Collections.unmodifiableMap()。

分步演示:groupingBy 的分组结果是怎么真实一步步搭起来的

每一行都是真实运行这段代码得到的结果。

groupingBy 的下游收集器真实决定分组后的聚合方式,可以真实嵌套多级;partitioningBy 对空输入真实和 groupingBy 表现不同
这一步新验证的分组

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,分组统计结果和Map类型重复运行完全一致(HashMap的key遍历顺序在同一次JVM运行内是稳定的)。
    • 和HashMap 基础那篇一起看,能理解为什么groupingBy默认结果的顺序不能依赖——它的底层容器和普通HashMap遵循同一套哈希分布规则。
    • 没有涉及:Collectors.partitioningBy()(按布尔条件二分,groupingBy的特化版本)与groupingBy(布尔条件)的真实性能/语义差异、并行流下groupingBy的真实线程安全实现方式(基于ConcurrentMap变体)——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号