CodeGym /課程 /JAVA 25 SELF /進階收集器

進階收集器

JAVA 25 SELF
等級 31 , 課堂 4
開放

1. 前言

當你透過 Stream API 操作集合時,往往不只是要分組元素,還會希望立刻對它們做點事:轉換、過濾,或收集成其他型態的集合。為此,Java 提供了 downstream 收集器——它們是套用在每個群組或資料子集上的巢狀收集器。

什麼是 downstream collector?

它是應用於分組或分割結果的收集器。舉例來說,你可以使用 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
    ));

先收集成一般 List,接著將其設為不可變。

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:忽略不可變性。 若回傳一般 List,其他人可能會修改它。請使用 toUnmodifiableList/Set/MapcollectingAndThen 來「凍結」結果。

錯誤 №2:選錯 downstream 收集器。 若需要在群組內轉換元素——使用 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