Effective Java Item 46:偏好無副作用的函式
整理 Effective Java 書中 Item 46: Prefer side-effect-free functions in streams 心得筆記
主旨:Streams 核心原則就是「無副作用」
Java 的 streams API 雖然語法簡潔、功能強大,但若沒有掌握其「函數式編程」的核心精神,寫出來的程式碼可能又臭又長、難讀又難維護。本篇要講的,就是 streams 最重要的設計原則:偏好無副作用的函式(side-effect-free functions)。
簡單來說,「副作用」是指函式除了回傳值之外,還會修改外部狀態。例如把某個值加進 map 或 list,就是副作用。真正的函數式風格應該避免這種做法,讓每個階段的處理都只是資料的轉換,不牽涉修改其他東西。
點出問題:用 forEach 做事是錯誤的用法
來看一個寫得不好的範例,這段程式碼是為了統計文字檔中每個單字出現的次數:
雖然語法上用了 streams、lambdas、method reference,但實際上這段完全是用迴圈的思維在操作。整個計算是靠外部的 freq
map 來實作,這樣的寫法 有副作用,而且不能平行處理、可讀性也不佳。
範例:正確的寫法用 collect
來看一個正確的版本,只用一行就能做出一樣的事情,還更清楚易讀:
這裡用 Collectors.groupingBy
和 Collectors.counting()
,不但沒有副作用,還讓意圖更清晰:我要把所有單字轉小寫後分組,然後統計每組的數量。
劃重點:什麼情況下要避免副作用?
在 stream 中,只要你在 lambda 裡面修改外部變數、收集資料進外部集合、操作外部資源,幾乎都是副作用。最常見的就是錯把 forEach
拿來做邏輯處理,而不是純輸出。以下是正確用法和錯誤用法的區別:
功能 | 錯誤用法(有副作用) | 正確用法(無副作用) |
---|---|---|
統計單字出現次數 | forEach + merge | groupingBy + counting |
累積字串 | 在 lambda 中修改 StringBuilder | 用 Collectors.joining() |
對集合做加總 | 外部定義 sum 然後在 lambda 裡加 | 用 reduce() 或 mapToInt().sum() |
真實世界範例:熱門單字前十名
假設你已經算出每個單字的出現次數,想印出出現頻率最高的十個單字:
這邊用 comparing(freq::get).reversed()
來排序,然後取前十個。全程無副作用,清楚易懂,而且能輕鬆平行化。
小結:Streams 是風格,不只是語法糖
- 不要把 streams 當成只是另一種 for-each 寫法。
- Lambda 裡面不要修改外部狀態,保持函式純粹。
- 把複雜的計算邏輯交給
Collectors
。 forEach
只適合最後印資料,不要用來做邏輯處理。- 需要學會用
groupingBy
、toMap
、joining
等 collector,它們是讓 streams 發揮最大效益的關鍵。
學會了這個原則之後,你會發現 stream 不只是語法漂亮,更是寫出簡潔、安全、高效 Java 程式碼的最佳工具。