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 做事是錯誤的用法
來看一個寫得不好的範例,這段程式碼是為了統計文字檔中每個單字出現的次數:
// 壞例子:用了 stream 但沒用對風格
Map<String, Long> freq = new HashMap<>();
try (Stream<String> words = new Scanner(file).tokens()) {
words.forEach(word -> {
freq.merge(word.toLowerCase(), 1L, Long::sum);
});
}
雖然語法上用了 streams、lambdas、method reference,但實際上這段完全是用迴圈的思維在操作。整個計算是靠外部的 freq map 來實作,這樣的寫法 有副作用,而且不能平行處理、可讀性也不佳。
範例:正確的寫法用 collect
來看一個正確的版本,只用一行就能做出一樣的事情,還更清楚易讀:
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
freq = words.collect(groupingBy(String::toLowerCase, counting()));
}
這裡用 Collectors.groupingBy 和 Collectors.counting(),不但沒有副作用,還讓意圖更清晰:我要把所有單字轉小寫後分組,然後統計每組的數量。
劃重點:什麼情況下要避免副作用?
在 stream 中,只要你在 lambda 裡面修改外部變數、收集資料進外部集合、操作外部資源,幾乎都是副作用。最常見的就是錯把 forEach 拿來做邏輯處理,而不是純輸出。以下是正確用法和錯誤用法的區別:
| 功能 | 錯誤用法(有副作用) | 正確用法(無副作用) |
|---|---|---|
| 統計單字出現次數 | forEach + merge | groupingBy + counting |
| 累積字串 | 在 lambda 中修改 StringBuilder | 用 Collectors.joining() |
| 對集合做加總 | 外部定義 sum 然後在 lambda 裡加 | 用 reduce() 或 mapToInt().sum() |
真實世界範例:熱門單字前十名
假設你已經算出每個單字的出現次數,想印出出現頻率最高的十個單字:
List<String> topTen = freq.keySet().stream()
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
這邊用 comparing(freq::get).reversed() 來排序,然後取前十個。全程無副作用,清楚易懂,而且能輕鬆平行化。
小結:Streams 是風格,不只是語法糖
- 不要把 streams 當成只是另一種 for-each 寫法。
- Lambda 裡面不要修改外部狀態,保持函式純粹。
- 把複雜的計算邏輯交給
Collectors。 forEach只適合最後印資料,不要用來做邏輯處理。- 需要學會用
groupingBy、toMap、joining等 collector,它們是讓 streams 發揮最大效益的關鍵。
學會了這個原則之後,你會發現 stream 不只是語法漂亮,更是寫出簡潔、安全、高效 Java 程式碼的最佳工具。