[Learn] Java Stream:filter、map、reduce 與 collect
/ 6 min read
Updated:Table of Contents
處理 collection 時,傳統 loop 經常同時負責走訪、判斷、轉換與保存結果
Stream API 把這些動作拆成可以依序組合的 operation,讓程式直接描述資料要經過哪些步驟
這篇開始前,建議先理解 Lambda 與 functional interface
這篇會專注整理以下內容
- Stream pipeline 的組成方式
filter如何篩選資料map與flatMap如何轉換資料reduce與collect如何產生結果- lazy evaluation 與 single-use Stream
- 什麼情況繼續使用一般 loop 會更清楚
Stream 是資料處理流程
先準備一組學生資料
import java.util.List;
record Student(String name, int score) {}
List<Student> students = List.of( new Student("Amy", 82), new Student("Ben", 55), new Student("Cara", 91), new Student("Dan", 60));Stream 不會保存另一份資料,也不會直接修改原本的 collection
它描述元素要經過哪些步驟
資料來源 → intermediate operation → intermediate operation → terminal operation例如找出及格學生的姓名
List<String> passedNames = students.stream() .filter(student -> student.score() >= 60) .map(Student::name) .sorted() .toList();
System.out.println(passedNames); // [Amy, Cara, Dan]stream()從 collection 建立 Streamfilter只留下符合條件的元素map把Student轉成姓名Stringsorted排序元素toList結束 pipeline 並產生新 List
Stream.toList() 回傳的 List 不能新增、刪除或替換元素
需要指定 collection 實作時,可以使用 collect
import java.util.ArrayList;import java.util.stream.Collectors;
ArrayList<String> mutableNames = students.stream() .map(Student::name) .collect(Collectors.toCollection(ArrayList::new));filter 留下符合條件的元素
filter 接收 Predicate<T>,每個元素都會得到 true 或 false
List<Student> passedStudents = students.stream() .filter(student -> student.score() >= 60) .toList();可以把條件先命名,讓 pipeline 更接近業務語意
Predicate<Student> hasPassed = student -> student.score() >= 60;Predicate<Student> isExcellent = student -> student.score() >= 90;
List<Student> excellentStudents = students.stream() .filter(hasPassed.and(isExcellent)) .toList();多個 filter 也可以依序排列
List<Student> result = students.stream() .filter(student -> student.score() >= 60) .filter(student -> student.name().startsWith("A")) .toList();filter 不應修改元素,只需要回答這個元素是否保留
map 把元素轉成另一種資料
map 接收 Function<T, R>,輸入與輸出可以是不同型別
List<String> labels = students.stream() .map(student -> student.name() + ":" + student.score()) .toList();這段 pipeline 的型別變化如下
Stream<Student> ↓ mapStream<String> ↓ toListList<String>如果轉換結果仍然是 collection,直接使用 map 會得到巢狀結構
List<List<String>> nested = List.of("java stream", "date time").stream() .map(text -> List.of(text.split(" "))) .toList();需要把每一層元素攤平時使用 flatMap
import java.util.Arrays;
List<String> words = List.of("java stream", "date time").stream() .flatMap(text -> Arrays.stream(text.split(" "))) .toList();
System.out.println(words); // [java, stream, date, time]reduce 把多個元素合成一個結果
reduce 會反覆把目前結果與下一個元素合併
int totalScore = students.stream() .map(Student::score) .reduce(0, Integer::sum);
System.out.println(totalScore); // 288這裡的 0 是 identity value,空 Stream 也會回傳 0
0 + 82 → 8282 + 55 → 137137 + 91 → 228228 + 60 → 288沒有提供 identity value 時,Stream 可能沒有結果,所以回傳 Optional
int highestScore = students.stream() .map(Student::score) .reduce(Integer::max) .orElse(0);reduce function 應該符合 associative,才能在不同分組與 parallel stream 中得到一致結果
加法適合 reduce,減法則不符合這個條件
(10 - 5) - 2 = 310 - (5 - 2) = 7要建立 List、Map 或分組結果時,通常使用 collect,不要用 reduce 不斷複製 mutable container
import java.util.Map;import java.util.stream.Collectors;
Map<Boolean, List<Student>> studentsByPassStatus = students.stream() .collect(Collectors.partitioningBy(student -> student.score() >= 60));intermediate operation 是 lazy 的
filter、map 與 sorted 都是 intermediate operation
它們會回傳新的 Stream,但在 terminal operation 出現前不會開始走訪資料
var pipeline = students.stream() .filter(student -> { System.out.println("檢查 " + student.name()); return student.score() >= 60; });
// 到這裡仍然不會輸出任何內容long count = pipeline.count();count、toList、collect、reduce 與 forEach 都是 terminal operation,會觸發 pipeline
Stream 完成 terminal operation 後就已經 consumed,不能再次使用
var stream = students.stream();
long count = stream.count();// stream.toList(); // IllegalStateException需要再次處理時,從 collection 建立新的 Stream
Stream pipeline 中也應避免修改外部狀態
List<String> names = new ArrayList<>();
// 不建議使用副作用收集結果students.stream() .map(Student::name) .forEach(names::add);直接使用 toList 或 collect 更能表達目的,也不會在 parallel stream 中遇到共享狀態問題
常見錯誤
忘記 terminal operation
只有 filter 與 map 不會開始處理資料
需要用 toList、count、reduce 或其他 terminal operation 取得結果
重複使用同一個 Stream
Stream 是 single-use pipeline
terminal operation 完成後應從原始 collection 建立新的 Stream
在 pipeline 中修改共享狀態
副作用會讓處理順序與 parallel execution 更難推理
優先透過 map 轉換,並用 toList、reduce 或 collect 產生結果
結論
- Stream pipeline 由資料來源、intermediate operation 與 terminal operation 組成
- filter 負責篩選,map 負責轉換,flatMap 負責攤平巢狀結果
- reduce 適合把元素合併成單一結果,collect 適合建立 collection 或分組結果
- intermediate operation 是 lazy,terminal operation 才會觸發資料走訪
- Stream 完成 terminal operation 後不能重複使用
- pipeline 中應避免修改共享狀態
- Stream 讓簡單的資料轉換更容易閱讀,但不需要取代所有 loop
參考資料
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/stream/package-summary.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/stream/Stream.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/stream/Collectors.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/function/Predicate.html
- https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/function/Function.html