1. 引言
当你通过 Stream API 处理集合时,往往不仅要对元素分组,还需要立刻对其进行处理:转换、过滤、收集到另一种集合类型。为此,Java 提供了下游收集器(downstream collectors)——它们是应用在每个分组或数据子集上的嵌套收集器。
什么是 downstream 收集器?
它是应用于分组或分区结果的收集器。比如,你可以用 groupingBy 按课程对学生分组,而在每个分组内部只收集姓名,或只收集优秀生(例如 GPA 阈值为 4.5)。
示例
mapping
允许在收集前对分组内的元素进行转换。
Map<Integer, List<String>> namesByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.mapping(Student::getName, Collectors.toList())
));
- 按课程对学生分组。
- 对每个分组只收集姓名(而不是整个对象)。
filtering
允许在分组内部过滤元素(例如,仅保留 GPA >= 4.5)。
Map<Integer, List<Student>> honorsByCourse = students.stream()
.collect(Collectors.groupingBy(
Student::getCourse,
Collectors.filtering(s -> s.getGpa() >= 4.5, Collectors.toList())
));
在每个分组中只保留优秀的学生。
flatMapping
允许在分组内“展开”嵌套集合。
Map<String, Set<String>> tagsByAuthor = books.stream()
.collect(Collectors.groupingBy(
Book::getAuthor,
Collectors.flatMapping(
book -> book.getTags().stream(),
Collectors.toSet()
)
));
为每位作者收集其所有图书的唯一标签。
partitioningBy 与 downstream
与 groupingBy 类似,但根据布尔条件分成两组。
Map<Boolean, List<String>> namesByPassed = students.stream()
.collect(Collectors.partitioningBy(
s -> s.getGpa() >= 3.0,
Collectors.mapping(Student::getName, Collectors.toList())
));
将学生分为通过与未通过,每组内——只保留姓名。
2. teeing:用两个收集器同时聚合
有时需要对流同时计算多个聚合值:例如总和与平均值,或最小值与最大值。为此,Java 12+ 提供了收集器 teeing。
teeing 如何工作?
你传入两个收集器以及一个用于合并它们结果的函数。
语法:
Collectors.teeing(collector1, collector2, (result1, result2) -> ...)
示例
最小值 + 最大值
Optional<MinMax> minMax = numbers.stream()
.collect(Collectors.teeing(
Collectors.minBy(Integer::compareTo),
Collectors.maxBy(Integer::compareTo),
(min, max) -> min.isPresent() && max.isPresent() ? new MinMax(min.get(), max.get()) : null
));
同时求出最小值和最大值。
总和 + 平均值
Result result = numbers.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Integer::intValue),
Collectors.averagingInt(Integer::intValue),
(sum, avg) -> new Result(sum, avg)
));
得到一个包含总和与平均值的对象。
示例:薪资报表
SalaryStats stats = employees.stream()
.collect(Collectors.teeing(
Collectors.summingInt(Employee::getSalary),
Collectors.averagingInt(Employee::getSalary),
SalaryStats::new
));
在 SalaryStats 中同时保存总和和平均值。
3. toUnmodifiableList/Set/Map 与 collectingAndThen 用于“冻结”
在现代版本的 Java 中,出现了创建后不可修改的集合——不可变集合(unmodifiable)。这在 API 设计中很实用,可以避免结果被意外修改。
toUnmodifiableList/Set/Map
- 返回不可变集合。
- 任何尝试添加/删除元素都会抛出 UnsupportedOperationException。
示例:
List<String> names = students.stream()
.map(Student::getName)
.collect(Collectors.toUnmodifiableList());
Map<Integer, Student> byId = students.stream()
.collect(Collectors.toUnmodifiableMap(Student::getId, Function.identity()));
collectingAndThen
允许对收集器的结果应用一个函数——例如“冻结”集合。
List<String> names = students.stream()
.map(Student::getName)
.collect(Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
));
先收集到普通列表,然后将其设为不可变。
Set 示例:
Set<String> tags = books.stream()
.flatMap(book -> book.getTags().stream())
.collect(Collectors.collectingAndThen(
Collectors.toSet(),
Set::copyOf // Java 10+
));
4. 流水线案例
报表与分段统计
借助高级收集器,可以“一行搞定”复杂的报表与统计。
示例:各部门平均薪资
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingInt(Employee::getSalary)
));
示例:各类别最贵的前 3 个商品
Map<String, List<Product>> top3ByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::getCategory,
Collectors.collectingAndThen(
Collectors.toList(),
list -> list.stream()
.sorted(Comparator.comparing(Product::getPrice).reversed())
.limit(3)
.toList()
)
));
将不可变结果作为 API 契约
如果你的方法返回不可修改的集合,这能避免偶然错误,让 API 更加可靠。
public List<String> getTags() {
return tags.stream()
.collect(Collectors.toUnmodifiableList());
}
用户将无法执行 getTags().add("新标签") —— 会抛出异常。
示例:包含多个聚合的报表(teeing)
public SalaryReport getSalaryReport(List<Employee> employees) {
return employees.stream()
.collect(Collectors.teeing(
Collectors.averagingInt(Employee::getSalary),
Collectors.summingInt(Employee::getSalary),
SalaryReport::new
));
}
在 SalaryReport 中同时保存平均薪资与总薪资。
5. 常见错误与细节
错误 1:忽视不可变性。 如果返回普通列表,它可能被修改。使用 toUnmodifiableList/Set/Map 或 collectingAndThen 来“冻结”结果。
错误 2:选择了错误的下游收集器。 若需要在分组内转换元素——使用 mapping;若需要过滤——使用 filtering;若需要“展开”嵌套集合——使用 flatMapping。
错误 3:UnsupportedOperationException。 当尝试修改通过 toUnmodifiableList/Set/Map 收集或通过 collectingAndThen“冻结”的集合时会出现。
错误 4:唯一性丢失/键冲突。 toUnmodifiableSet 要求元素唯一,而 toUnmodifiableMap 要求键唯一;否则会在收集过程中抛出异常。
GO TO FULL VERSION