CodeGym /课程 /JAVA 25 SELF /高级收集器

高级收集器

JAVA 25 SELF
第 31 级 , 课程 4
可用

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/MapcollectingAndThen 用于“冻结”

在现代版本的 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/MapcollectingAndThen 来“冻结”结果。

错误 2:选择了错误的下游收集器。 若需要在分组内转换元素——使用 mapping;若需要过滤——使用 filtering;若需要“展开”嵌套集合——使用 flatMapping

错误 3:UnsupportedOperationException 当尝试修改通过 toUnmodifiableList/Set/Map 收集或通过 collectingAndThen“冻结”的集合时会出现。

错误 4:唯一性丢失/键冲突。 toUnmodifiableSet 要求元素唯一,而 toUnmodifiableMap 要求键唯一;否则会在收集过程中抛出异常。

1
调查/小测验
分组与聚合第 31 级,课程 4
不可用
分组与聚合
Stream API:分组与聚合
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION