java group by常见用法

java 复制代码
// 1. 按部门分组,默认每桶装成 List<Employee>
Map<String, List<Employee>> byDept = Employee.samples().stream()
        .collect(Collectors.groupingBy(Employee::getDepartment));
// {产品=[王五, 赵六], 技术=[张三, 李四, 周九], 运营=[钱七, 孙八]}
java 复制代码
// 2. 每个部门人数
Map<String, Long> counts = emps.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment, Collectors.counting()));
// {产品=2, 技术=3, 运营=2}

// 3. 每个部门平均薪资
Map<String, Double> avgSalary = emps.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment,
                Collectors.averagingInt(Employee::getSalary)));

// 4. 每个部门薪资最高的员工(maxBy 会包在 Optional 里)
Map<String, Optional<Employee>> topOfDept = emps.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment,
                Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))));

groupingBy 的下游几乎可以是任何 Collector------counting / summingInt / averagingInt / maxBy / minBy / mapping / toList / joining / reducing把它们像乐高一样拼起来 。实在记不住,记一句「先分桶,再对每桶做一次下游收集」。

java 复制代码
// 5. 多级分组:按部门、再按年龄段(30岁以上/以下)
Map<String, Map<String, List<Employee>>> multi = emps.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment,
                Collectors.groupingBy(e -> e.getAge() >= 30 ? "资深" : "新人")));

// 6. mapping + toList:每个部门员工的薪资列表(先 map 再收集)
Map<String, List<Integer>> salariesByDept = emps.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment,
                Collectors.mapping(Employee::getSalary, Collectors.toList())));

// 7. 保持输入顺序(默认 HashMap 无序)
Map<String, List<Employee>> byDeptOrdered = emps.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment,
                java.util.LinkedHashMap::new,    // 指定 map factory
                Collectors.toList()));

map重复key问题

java 复制代码
// ❌ 重复 key 直接抛 IllegalStateException
Map<String, Integer> bad = Order.samples().stream()
        .collect(Collectors.toMap(Order::getCustomer, o -> 1));
// Duplicate key 张三 (attempted merging values 1 and 1)

解决:传第三个参数 mergeFunction,告诉它遇到重复 key 时怎么合并

java 复制代码
// 保留旧值
Map<String, Double> firstAmount = Order.samples().stream()
        .collect(Collectors.toMap(Order::getCustomer, Order::getAmount, (a, b) -> a));

// 求和(计数场景最常用)
Map<String, Double> sumAmount = Order.samples().stream()
        .collect(Collectors.toMap(Order::getCustomer, Order::getAmount, Double::sum));
// {张三=380.0, 李四=380.0, 王五=230.0, 赵六=90.0}

再加一个第四参数 mapFactory,可以指定返回 TreeMap(按 key 排序)或 LinkedHashMap(保持插入顺序):

java 复制代码
// 返回 TreeMap,自动按客户名排序
TreeMap<String, Double> sorted = Order.samples().stream()
        .collect(Collectors.toMap(Order::getCustomer, Order::getAmount,
                Double::sum, TreeMap::new));

还有一个特别容易踩的坑 :valueMapper 里返回的是同一个共享可变对象时,多个 key 会指向同一个引用,往任一 key 的 value 里加东西,其他 key 都会跟着被改:

java 复制代码
// ❌ 共享可变对象作 value,所有 key 拿到的是同一个 ArrayList
List<String> shared = new ArrayList<>();
Map<String, List<String>> broken = Order.samples().stream()
        .collect(Collectors.toMap(Order::getCustomer, o -> shared, (a, b) -> a));
broken.get("张三").add("张三的第一笔");
broken.get("王五").add("王五的第一笔");
System.out.println(broken.get("张三"));  // [张三的第一笔, 王五的第一笔]
System.out.println(broken.get("王五"));  // [张三的第一笔, 王五的第一笔]  ← 串了!

解决办法:valueMapper 里每次都 new 一个 ,比如 o -> new ArrayList<>()。这点跟 groupingBymapping(..., toList()) 形成对照------后者是官方已经帮你把"每个 key 单独 new 一次"做好了,所以你不会踩这个坑。

只切两组(true / false)的时候用 partitioningBy,比 groupingBy 简洁:

java 复制代码
// 按薪资是否 >= 25000 分两组
Map<Boolean, List<Employee>> parts = emps.stream()
        .collect(Collectors.partitioningBy(e -> e.getSalary() >= 25000));

groupingBy 的关键区别

  • partitioningBy 的 key 永远是 Boolean任何一边为空也一定会存在 (即使 filter().collect(...) 之后两边都空,map 里仍然有 truefalse 两个 key)。

  • groupingBy 只在你分类器能命中的 key 才会出现。

java 复制代码
// partition + summingDouble:每组订单金额总和
Map<Boolean, Double> sumByPaid = Order.samples().stream()
        .collect(Collectors.partitioningBy(
                o -> o.getStatus() == Order.Status.PAID || o.getStatus() == Order.Status.COMPLETED,
                Collectors.summingDouble(Order::getAmount)));

选用口诀:只切两组 boolean 用 partitioningBy,切多个 key 值用 groupingBy

java 复制代码
List<String> tags = Arrays.asList("Java", "MySQL", "Redis", "Docker");

tags.stream().collect(Collectors.joining());                  // JavaMySQLRedisDocker
tags.stream().collect(Collectors.joining(","));               // Java,MySQL,Redis,Docker
tags.stream().collect(Collectors.joining(", ", "[", "]"));    // [Java, MySQL, Redis, Docker]

要 count / sum / min / max / avg 全都想要 的时候,用 summarizingInt 一个顶五个:

java 复制代码
IntSummaryStatistics stat = Employee.samples().stream()
        .collect(Collectors.summarizingInt(Employee::getSalary));
// IntSummaryStatistics{count=7, sum=195000, min=18000, average=27857.142857, max=45000}

maxBy / minBy 因为流的元素可能为零个,会用 Optional 包一层;想直接拿值要么 Optional.get()(小心),要么用后面讲的 collectingAndThen 拆包。

六、reducing:自定义合并规则的通用收集器

上面五个全是 reducing 的特例。如果业务上要的不是简单加减,而是自定义合并逻辑 (比如字符串拼接、复杂加权),用 reducing

java 复制代码
// 找薪资最高的员工
Optional<Employee> top = emps.stream()
        .collect(Collectors.reducing((a, b) -> a.getSalary() >= b.getSalary() ? a : b));

// 求和(identity 是初值,相当于 stream().mapToInt().sum())
int total = emps.stream()
        .collect(Collectors.reducing(0, Employee::getSalary, Integer::sum));

七、mapping:下游里先转换再收集

java 复制代码
// 每个部门员工的姓名列表(不需要先 map 整个 Employee)
Map<String, List<String>> namesByDept = emps.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment,
                Collectors.mapping(Employee::getName, Collectors.toList())));

收完之后想把结果再处理一道(拆 Optional、加固为不可变、转类型),用 collectingAndThen

java 复制代码
// 1. maxBy 的 Optional 拆出来
Employee top = emps.stream()
        .collect(Collectors.collectingAndThen(
                Collectors.maxBy(Comparator.comparingInt(Employee::getSalary)),
                Optional::get));

// 2. 分组后让每组 List 不可变(防止下游改坏)
Map<String, List<Employee>> unmod = emps.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment,
                Collectors.collectingAndThen(
                        Collectors.toList(),
                        Collections::unmodifiableList)));

// 3. counting 后转成 int
Integer count = emps.stream()
        .collect(Collectors.collectingAndThen(Collectors.counting(), Long::intValue));
java 复制代码
List<String> list = stream.collect(Collectors.toList());                                  // 通用
Set<String> set = stream.collect(Collectors.toSet());                                     // 去重
TreeSet<String> sorted = stream.collect(Collectors.toCollection(TreeSet::new)); 
相关推荐
元Y亨H1 天前
告别依赖地狱与打包崩溃:Python 项目环境治理与 PyInstaller 避坑实践
python
用户094248568031 天前
第12章:JDK 诊断工具箱——jps / jstat / jmap / jcmd / jhsdb
java·jvm
aramae1 天前
Python 使用库:标准库与第三方库实战
服务器·开发语言·windows·python
IamZJT_1 天前
Agent 系统工程 01|模型之外,Harness 到底该负责什么?
人工智能·python·程序员
沧海一笑-dj1 天前
【Python】Python学习笔记-Python 核心基础
人工智能·python·ai·解释型语言
用户76855341255381 天前
Python 并发怎么选?一篇讲清 threading、multiprocessing、asyncio 的边界
python
景熙55231 天前
14.Java 集合框架从入门到源码万字详解(含 ArrayList/LinkedList/HashMap 源码、泛型通配符、红黑树)
java·开发语言·数据结构·算法
梦想不只是梦与想1 天前
环境管理系统:Conda(一)
python·conda·环境隔离
五仁火烧1 天前
大模型开发: MCP
java·大模型·mcp
yuzhiboyouye1 天前
XML写接口适用场景举例
java·服务器·前端