1. 建立串流
要使用 Stream API,首先必須從某個集合或陣列取得一個串流。
建立串流的範例
// 來自 List
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();
// 來自陣列
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);
// 來自個別值
Stream<String> letters = Stream.of("A", "B", "C");
重點:
- list.stream() — 用於集合
- Arrays.stream(array) — 用於陣列
- Stream.of(...) — 用於個別值
我們應用中的情境範例
假設我們有一個使用者清單:
List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();
中間操作與終端操作
關鍵重點:Stream API 的操作分為兩種類型。
- 中間操作(例如,filter、map、distinct)— 描述處理步驟。它們會回傳新的串流,但本身不會觸發任何計算。
- 終端操作(例如,collect、forEach、count)— 會啟動整個管線並產生結果。
串流是「延遲求值」的:在呼叫終端操作之前,不會進行任何計算。這也是為什麼我們常以 collect(...) 結尾 — 那是把串流轉回集合或其他結果的地方。
2. filter 操作:依條件篩選元素
filter 是一個中間操作,只保留符合指定條件的元素。
方法簽名
Stream<T> filter(Predicate<? super T> predicate);
Predicate 是一個函式介面,接收一個元素並回傳 true(保留)或 false(剔除)。
範例:只保留偶數
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4, 6]
發生了什麼?
- n -> n % 2 == 0 — 這個 Lambda 運算式檢查整數是否可被 2 整除。
- filter 只會留下偶數。
範例:篩選以「A」開頭的名字
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<String> aNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(aNames); // [Anna, Alex, Alina]
重點: filter 不會修改原始集合 — 它會建立一個只含所需元素的新串流。
3. map 操作:把元素轉換成其他東西
map 是轉換操作。它對串流中的每個元素套用函式,並回傳新的元素。
方法簽名
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Function 是一個介面,接收一個元素並回傳某個值(可以是不同型別)。
範例:取得字串長度
List<String> names = List.of("Anna", "Boris", "Alex");
List<Integer> nameLengths = names.stream()
.map(name -> name.length())
.collect(Collectors.toList());
System.out.println(nameLengths); // [4, 5, 4]
發生了什麼?
- map 把字串轉成它的長度(name -> name.length())。
- 結果得到的是一個數值串流。
範例:將字串轉為大寫
List<String> names = List.of("Anna", "Boris", "Alex");
List<String> upperNames = names.stream()
.map(name -> name.toUpperCase())
.collect(Collectors.toList());
System.out.println(upperNames); // [ANNA, BORIS, ALEX]
4. collect 操作:把結果收集回集合
collect 是終端操作,也就是它會結束串流,並將結果收集到集合或其他容器中。
方法簽名
<R, A> R collect(Collector<? super T, A, R> collector)
別被可怕的簽名嚇到!在 99% 的情況下,你會使用 Collectors 類別中現成的收集器。
Collectors 是個工具類別,內含一組「收集器」。它告訴串流要以哪種形態收集結果:清單、集合、字串等。
範例:
- Collectors.toList() — 收集為 List
- Collectors.toSet() — 收集為 Set
- Collectors.joining(", ") — 收集為以逗號分隔的字串
換句話說,Collectors 有點像各種不同形狀的盒子,你把串流元素打包進對應的盒子裡。
範例:收集為 List
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
範例:收集為 Set
Set<String> uniqueNames = names.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
範例:把字串以逗號串接
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result); // Anna, Boris, Alex
5. 操作鏈:篩選 + 轉換 + 收集結果
Stream API 最強大的地方 — 可將操作串接成一條管線。
範例:取得以「A」開頭名字的長度
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<Integer> aNameLengths = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::length)
.collect(Collectors.toList());
System.out.println(aNameLengths); // [4, 4, 5]
逐步說明:
- .stream() — 從清單建立串流。
- .filter(name -> name.startsWith("A")) — 只保留以 "A" 開頭的名字。
- .map(String::length) — 將每個名字轉成它的長度。
- .collect(Collectors.toList()) — 將結果收集成清單。
對應的命令式寫法
以下是以「傳統」方式撰寫會長這樣:
List<Integer> result = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
result.add(name.length());
}
}
對比看看: Stream API — 一行即可,關注的是「做什麼」,而非「怎麼做」。
6. 練習:幾個短題目
來練習一下!所有範例都能在同一個檔案中執行 — 只要替換資料即可。
題目 1:只保留奇數並將其平方
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
List<Integer> oddSquares = numbers.stream()
.filter(n -> n % 2 != 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(oddSquares); // [1, 9, 25, 49]
題目 2:從字串清單取得它們的第一個字元清單
List<String> names = List.of("Anna", "Boris", "Alex");
List<Character> initials = names.stream()
.map(name -> name.charAt(0))
.collect(Collectors.toList());
System.out.println(initials); // [A, B, A]
題目 3:篩出長度大於 3 的字串並收集為 Set
List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");
Set<String> longWords = words.stream()
.filter(word -> word.length() > 3)
.collect(Collectors.toSet());
System.out.println(longWords); // [bear, elephant]
7. 使用 filter、map、collect 時的常見錯誤
錯誤 1:忘了 collect — 就沒有結果!
Stream API 很「懶」,就像窗台上的貓:在你呼叫終端操作之前(例如,collect 或 forEach),什麼都不會發生。若只寫 users.stream().filter(...).map(...); — 實際上不會做任何事。
錯誤 2:filter 與 map 的順序搞反了
有時新手會先做 map,再做 filter。例如,names.stream().map(String::length).filter(len -> len > 3) — 產生的是數字,而不是字串。若你要的是長度大於 3 的字串,請先篩選,再轉換。
錯誤 3:忽略了不可變性
Stream API 的操作不會改變原集合!它們會回傳新的結果。執行 List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); 之後 — 集合 names 仍然保持不變。
錯誤 4:嘗試使用外部可變的清單
不要這樣做:
List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));
應改用 collect — 更安全也更精簡。
錯誤 5:NullPointerException
如果集合中可能包含 null 元素,對 null 呼叫 name.startsWith("A") 會出錯。若有可能,請先過濾掉 null:
.filter(name -> name != null && name.startsWith("A"))
GO TO FULL VERSION