CodeGym /課程 /JAVA 25 SELF /Stream API 的基本操作:map、filter、collect

Stream API 的基本操作:map、filter、collect

JAVA 25 SELF
等級 30 , 課堂 1
開放

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 的操作分為兩種類型。

  • 中間操作(例如,filtermapdistinct)— 描述處理步驟。它們會回傳新的串流,但本身不會觸發任何計算。
  • 終端操作(例如,collectforEachcount)— 會啟動整個管線並產生結果。

串流是「延遲求值」的:在呼叫終端操作之前,不會進行任何計算。這也是為什麼我們常以 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]

逐步說明:

  1. .stream() — 從清單建立串流。
  2. .filter(name -> name.startsWith("A")) — 只保留以 "A" 開頭的名字。
  3. .map(String::length) — 將每個名字轉成它的長度。
  4. .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 很「懶」,就像窗台上的貓:在你呼叫終端操作之前(例如,collectforEach),什麼都不會發生。若只寫 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"))
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION