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)); 
相关推荐
SKH.23 分钟前
Linux软件编程(6)线程间通信
java·开发语言·数据库
Rain的Java大神之路28 分钟前
PC版网站被狂刷怎么处理
java·数据库·redis·后端·mysql·web安全·运维开发
whcyhhh42 分钟前
头歌实践教学平台:数据科学与大数据技术导论(十六)
大数据·开发语言·python
萌动的小火苗42 分钟前
数据结构面试题【无答案】
c语言·开发语言·数据结构·链表
宸津-代码粉碎机44 分钟前
AI工程化高阶实战|从Demo到生产落地核心架构、全套源码与避坑指南
java·大数据·开发语言·人工智能·python·架构
XR1234567882 小时前
办公网自主创新建设怎么选?信创办公场景的选型逻辑
开发语言·php
泡海椒2 小时前
告别手写 HTTP 模板!JQuick-Curl:直接把 curl 命令跑在 Java 中
java·网络协议·http
珍珠先生2 小时前
第2章 集合框架深度:HashMap 原理与并发集合
java
郝学胜-神的一滴2 小时前
Effective Python 条款4:字符串格式化大乱斗
开发语言·网络·python·程序人生·软件工程