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<>()。这点跟 groupingBy 的 mapping(..., 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 里仍然有 true 和 false 两个 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)); 
相关推荐
czq_26867194877 分钟前
Python打卡第34天
开发语言·python·机器学习
计算机毕业编程指导师10 分钟前
大数据毕设选题推荐:基于Hadoop+Spark全球碳排放减排策略分析与可视化系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
大数据·hadoop·python·计算机·spark·毕业设计·碳排放减排
sibylyue11 分钟前
SQLite 运行模式
java·jvm·sqlite
专业程序开发源11 分钟前
django便利店外卖后台管理系统19650-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·python·django·课程设计
JAVA面经实录91716 分钟前
Java高级后端 · 架构组件开发 + CodeReview(本岗位差异化核心)
java·架构·代码复审
toooooop818 分钟前
宝塔Python项目管理器:环境变量踩坑记录
java·jvm·python
大文说跨境19 分钟前
用 Python 自动化检测浏览器环境指纹:WebRTC 泄露、Canvas 哈希与时区一致性校验
python·自动化·webrtc
代码山河21 分钟前
Java语言特点:跨平台、面向对象、安全性详解
java·学习·架构·教程·面向对象·项目
马剑威(威哥爱编程)25 分钟前
【AI全栈后端12-09】Spring Boot 用流式输出做打字机对话:点下去不再干等
java·人工智能·spring boot·后端
步行cgn27 分钟前
Spring 事务传播行为详解
java·数据库·spring