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));