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/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
));
先收集成一般 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/Map 或 collectingAndThen 來「凍結」結果。
錯誤 №2:選錯 downstream 收集器。 若需要在群組內轉換元素——使用 mapping;若需要過濾——使用 filtering;若需要「展開」巢狀集合——使用 flatMapping。
錯誤 №3:UnsupportedOperationException。 嘗試修改透過 toUnmodifiableList/Set/Map 收集,或經由 collectingAndThen「凍結」的集合時會拋出此例外。
錯誤 №4:唯一性遺失/鍵衝突。 toUnmodifiableSet 要求元素唯一,而 toUnmodifiableMap 要求鍵唯一;否則在收集期間會拋出例外。
GO TO FULL VERSION