CodeGym /課程 /JAVA 25 SELF /forEach、peek 方法:副作用

forEach、peek 方法:副作用

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

1. forEach 方法:對元素的最終動作

在深入細節之前,先再次回顧什麼是終端操作與中間操作。

  • 中間操作(例如 filtermapdistinctpeek)——會回傳新的串流,且通常在呼叫終端操作之前不會真正執行。
  • 終端操作(例如 forEachcollectcountanyMatch)——會啟動串流元素的處理並回傳結果(或不回傳任何東西,如 forEach)。

在終端操作之後,串流被視為關閉,無法再對其套用其他操作。就像想把已經吃掉的冰淇淋再吃一次——做不到:串流已經「用過」了。

現在來認識兩個與副作用相關的重要方法:forEachpeek

forEach 做什麼?

forEach 是一個終端操作,會對串流中的每個元素執行指定的動作。通常用於輸出到螢幕、寫入日誌、統計計數與其他副作用(side effects)。

方法簽名:

void forEach(Consumer<? super T> action)

Consumer<T> 是一個函式式介面,接收一個參數且不回傳任何值(例如 System.out::println)。

範例:輸出列表中的所有元素

假設我們有一個使用者名稱的清單:

List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");

使用 Stream API 將所有使用者輸出到螢幕非常簡單:

users.stream().forEach(System.out::println);

輸出:

Anna
Boris
Alex
Alina
Dmitry

也可以使用 Lambda 表達式:

users.stream().forEach(name -> System.out.println("使用者: " + name));

輸出:

使用者: Anna
使用者: Boris
使用者: Alex
使用者: Alina
使用者: Dmitry

重要:forEach 會終結串流

呼叫 forEach 之後,串流就「被關閉」了。不能繼續鏈式呼叫:

users.stream()
     .filter(name -> name.startsWith("A"))
     .forEach(System.out::println)
     .map(String::toUpperCase); // 錯誤!流已經被關閉。

forEach 之後再呼叫其他方法會導致編譯錯誤:終端操作回傳的是 void,而不是新的串流。

2. peek 方法:只偷看,不介入

peek 是一個中間操作。它允許在處理流程的特定階段對每個元素執行動作,但不改變元素本身,也不會終結串流。

方法簽名:

Stream<T> peek(Consumer<? super T> action)
  • peek 會回傳新的串流,對其中每個元素執行 action
  • 通常用於除錯、日誌或監控串流狀態。

範例:過濾之後的日誌

List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");

List<Integer> nameLengths = users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("通過篩選: " + name))
    .map(String::length)
    .collect(Collectors.toList());

主控台輸出:

通過篩選: Anna
通過篩選: Alex
通過篩選: Alina

nameLengths 的內容:

[4, 4, 5]

哪些情境適合使用 peek?

  • 用來除錯整條操作鏈:查看每個階段發生了什麼。
  • 用於蒐集統計(例如計數)。
  • 在中間階段記錄日誌。

重要:peek 不應用來修改串流元素。轉換請使用 mappeek 是「偷看」,不是「介入」。

3. forEach 與 peek:有何差異?

方法 操作類型 何時使用 能否繼續鏈式呼叫? 最適用於
forEach
終端 在串流處理的最後階段 最終動作(輸出、記錄日誌、寫入資料庫)
peek
中間 在操作鏈的中間 除錯、中間階段日誌、計數

範例:使用上的差異

// forEach 範例
users.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .forEach(System.out::println); // 流在此終結

// peek 範例
users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("篩選通過: " + name))
    .map(String::toUpperCase)
    .collect(Collectors.toList()); // 可以繼續鏈式呼叫

務必記住

  • forEach 是不歸點:之後就無法再對串流做任何事。
  • peek 若後面沒有終端操作,則不保證會執行。只寫一串中間操作——什麼也不會發生。

4. 不那麼顯而易見的點:forEach 並非總是最佳選擇!

為何不該用 forEach 來修改集合?

許多新手會嘗試用 forEach 來修改集合的元素,甚至修改集合本身(例如刪除元素)。但這是個不佳的做法:串流並非為了修改原始集合而設計。

錯誤範例:

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Alex"));
names.stream().forEach(name -> {
    if (name.startsWith("A")) {
        names.remove(name); // 可能導致 ConcurrentModificationException!
    }
});

結果:執行階段錯誤——在遍歷串流時不可修改集合(ConcurrentModificationException)。

那麼 forEach 應該用在什麼地方?

  • 輸出到螢幕(例如列印報表)。
  • 寫入日誌。
  • 呼叫外部服務(例如傳送電子郵件)。
  • 蒐集統計(例如遞增計數器)。

5. 再談 peek:只用於除錯!

很多人會想用 peek 來修改元素,例如把使用者的年齡加一:

users.stream()
    .peek(user -> user.setAge(user.getAge() + 1)) // 不建議!
    .collect(Collectors.toList());

為什麼不好?

  • 這違反了宣告式與 Stream API 的純粹性。
  • 這樣的程式碼難以維護與測試。
  • 中間操作中的副作用可能導致隱晦的錯誤。

更好的作法是用 map 進行資料轉換:

List<User> olderUsers = users.stream()
    .map(user -> new User(user.getName(), user.getAge() + 1))
    .collect(Collectors.toList());

示意:forEach 與 peek 的差別

users.stream()
    .filter(...)         // 中間操作
    .peek(...)           // 中間操作,「偷看」
    .map(...)            // 中間操作
    .forEach(...)        // 終端操作,「執行動作」

說明:
— 在 forEach 之前的一切都可以組合、重排、加入。
— 在 forEach 之後,串流已關閉。

6. 使用 forEach 與 peek 時的常見錯誤

錯誤 №1:使用 peek 來修改資料。peek 僅用於觀察,而不是修改串流元素。要做轉換請用 map

錯誤 №2:以為 peek 一定會執行。peek 只有在其後存在終端操作(collectforEachcount 等)時才會執行。沒有終端操作,什麼也不會發生。

錯誤 №3:嘗試在 forEach 之後繼續串流。forEach 是終端操作,之後不能再呼叫其他串流方法。

錯誤 №4:在 forEach 內修改集合。在透過 forEach 迭代時修改原始集合(刪除或新增元素)會直接導致 ConcurrentModificationException

錯誤 №5:用 forEach 取代 collect 來蒐集結果。如果你想把元素蒐集到新的集合,請使用 collect(Collectors.toList()),而不是用 forEach 手動新增。這違反宣告式風格,且在多執行緒情境下可能導致錯誤。

1
任務
JAVA 25 SELF, 等級 30, 課堂 3
上鎖
叫學生到講台前 👩‍🏫
叫學生到講台前 👩‍🏫
1
任務
JAVA 25 SELF, 等級 30, 課堂 3
上鎖
智慧家庭資料流偵錯 🏠
智慧家庭資料流偵錯 🏠
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION