前置说明:基于 Java17,所有代码可直接复制运行;区分 分组 groupingBy(按任意 key)、分区 partitioningBy(固定 boolean key:true/false);同时包含多级分组、分组后聚合、分组后映射、分组排序、下游收集器、自定义 key、分组后过滤等高频场景。 依赖:
import java.util.stream.Collectors;
实体类(统一使用,所有案例共用)
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
// id、姓名、年级、班级、分数、性别
private Long id;
private String name;
private Integer grade; // 年级:1、2、3
private Integer clazz; // 班级:1、2
private Integer score;
private Boolean gender; // true=男,false=女
}
测试数据初始化:
List<Student> studentList = List.of(
new Student(1L, "张三", 1, 1, 88, true),
new Student(2L, "李四", 1, 1, 72, false),
new Student(3L, "王五", 1, 2, 95, true),
new Student(4L, "赵六", 2, 1, 59, false),
new Student(5L, "孙七", 2, 2, 82, true),
new Student(6L, "周八", 2, 2, 66, false),
new Student(7L, "吴九", 3, 1, 91, true),
new Student(8L, "郑十", 3, 2, 45, false)
);
一、partitioningBy 分区(boolean 二分,只有 true /false 两个 key)
partitioningBy(Predicate),适合二分类场景:及格 / 不及格、男 / 女、满足条件 / 不满足
1. 基础分区:分数 >=60 及格、不及格
// Map<Boolean, List<Student>> key只有true(及格) false(不及格)
Map<Boolean, List<Student>> passPartition = studentList.stream()
.collect(Collectors.partitioningBy(student -> student.getScore() >= 60));
System.out.println("及格:" + passPartition.get(true));
System.out.println("不及格:" + passPartition.get(false));
2. partitioningBy + 下游收集器:分区后直接统计数量
Map<Boolean, Long> passCount = studentList.stream()
.collect(Collectors.partitioningBy(
s -> s.getScore() >= 60,
Collectors.counting()
));
// {true=6, false=2}
3. partitioningBy + mapping:分区后只收集学生姓名
Map<Boolean, List<String>> passNameList = studentList.stream()
.collect(Collectors.partitioningBy(
s -> s.getScore() >= 60,
Collectors.mapping(Student::getName, Collectors.toList())
));
4. partitioningBy + 求平均分(下游 averagingInt)
Map<Boolean, Double> passAvgScore = studentList.stream()
.collect(Collectors.partitioningBy(
s -> s.getScore() >= 60,
Collectors.averagingInt(Student::getScore)
));
✅ 小结:partitioningBy 本质是 groupingBy 的特例,key 固定 Boolean;不会丢 key,就算没有符合 true 的数据,依然保留 key=true,value 是空集合。
二、groupingBy 分组(任意类型 key:Integer、String、Object)
groupingBy 三个重载:
groupingBy(Function classifier)→ 默认下游 toList (),容器 HashMapgroupingBy(Function classifier, Collector downstream)→ 指定下游收集器groupingBy(Function classifier, Supplier mapFactory, Collector downstream)→ 指定 Map 实现(LinkedHashMap 有序)
案例 1:基础分组,按年级分组(key = 年级,value = 该年级学生集合)
Map<Integer, List<Student>> groupByGrade = studentList.stream()
.collect(Collectors.groupingBy(Student::getGrade));
案例 2:分组后,下游 counting → 统计每个年级人数
Map<Integer, Long> gradeStudentCount = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.counting()
));
案例 3:分组后,summingInt 求和:每个年级总分
Map<Integer, Integer> gradeTotalScore = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.summingInt(Student::getScore)
));
案例 4:分组后 averagingInt:每个年级平均分
Map<Integer, Double> gradeAvgScore = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.averagingInt(Student::getScore)
));
案例 5:分组后 maxBy /minBy:每个年级最高分学生
Map<Integer, Optional<Student>> gradeMaxStudent = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.maxBy(Comparator.comparingInt(Student::getScore))
));
// 取值:gradeMaxStudent.get(1).ifPresent(System.out::println);
案例 6:分组后 mapping:分组只收集姓名,不存整个对象
Map<Integer, List<String>> gradeNameList = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.mapping(Student::getName, Collectors.toList())
));
案例 7:分组后收集为 Set(去重)
Map<Integer, Set<String>> gradeNameSet = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.mapping(Student::getName, Collectors.toSet())
));
案例 8:多级分组(高频!先按年级,再按班级分组)
Map <年级,Map < 班级,学生列表>>
Map<Integer, Map<Integer, List<Student>>> gradeClazzGroup = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.groupingBy(Student::getClazz)
));
多级分组下游继续聚合:先年级、再班级,统计每个班级人数
Map<Integer, Map<Integer, Long>> gradeClazzCount = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.groupingBy(
Student::getClazz,
Collectors.counting()
)
));
案例 9:分组保持有序(LinkedHashMap,分组后 key 顺序和流顺序一致)
默认 groupingBy 返回 HashMap,无序;需要有序传入 mapFactory
Map<Integer, List<Student>> groupByGradeOrdered = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
LinkedHashMap::new,
Collectors.toList()
));
案例 10:自定义分组 key(组合 key,年级 + 班级拼接字符串)
Map<String, List<Student>> groupByGradeClazz = studentList.stream()
.collect(Collectors.groupingBy(s -> s.getGrade() + "-" + s.getClazz()));
如果不想用 String 拼接,可以自定义对象作为 key(记得重写 equals+hashCode)
案例 11:分组后过滤(重要:先分组再过滤下游元素,不是 filter 先过滤流)
需求:按年级分组,每个分组里只保留及格学生 ❌ 错误:stream.filter 会直接丢掉不及格数据 ✅ 正确:groupingBy + mapping + filter(Collectors.filtering,Java9+)
// Java9+ 新增 Collectors.filtering
Map<Integer, List<Student>> gradePassStudent = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.filtering(s -> s.getScore() >= 60, Collectors.toList())
));
Java8 没有
Collectors.filtering,替代方案:分组后再遍历 map 过滤
案例 12:分组后 joining 拼接字符串
每个年级的学生名字用逗号拼接
Map<Integer, String> gradeNameJoin = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.mapping(Student::getName, Collectors.joining(","))
));
案例 13:分组后收集为单个值(reducing)
每个年级分数累加(和 summingInt 效果类似,reducing 更通用)
Map<Integer, OptionalInt> gradeReduceSum = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.mapping(Student::getScore, Collectors.reducing(Integer::sum))
));
案例 14:分组 + 分区 嵌套组合
先按年级分组,每个年级内部再分区及格 / 不及格
Map<Integer, Map<Boolean, List<Student>>> gradePassPartition = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.partitioningBy(s -> s.getScore() >= 60)
));
三、对比 & 踩坑清单
高频坑
Java8 无
Collectors.filtering(),分组内过滤要 Java9 及以上;groupingBy 默认 HashMap,key 无序;需要顺序必须指定 LinkedHashMap;
maxBy/minBy 返回 Optional,分组后取值一定要判空;
自定义对象作为分组 key,必须重写 equals () 和 hashCode (),否则分组失效;
不要在 groupingBy 的 classifier 函数里写重逻辑(数据库查询),会重复执行;
partitioningBy 不要用来做多分类,适合严格二分。
四、拓展:groupingBy + teeing(Java12+,一个分组同时拿多个聚合值)
一个分组同时拿到【人数、平均分】
Map<Integer, DoubleSummaryStatistics> gradeStat = studentList.stream()
.collect(Collectors.groupingBy(
Student::getGrade,
Collectors.summarizingInt(Student::getScore)
));
// summary 可以直接 getCount、getAverage、getMax、getMin本文原创作者:易君召,详见:https://www.yijunzhao.cc/about,转载请注明出处。
原文链接
欢迎访问