Effective Java Item 45:謹慎使用 streams
整理 Effective Java 書中 Item 45: Use streams judiciously 心得筆記
主旨
Java 8 推出的 streams API 是用來處理「大量資料的處理流程」的工具,它支援類似函式式的操作(例如 map、filter、collect 等),並能以流暢的語法串接多個處理階段。然而,streams 是一把雙面刃:用得好能讓程式簡潔清楚,用不好會讓人看不懂又難維護。
本篇要講的重點就是:該用的時候再用,避免過度使用。
點出問題
stream pipeline 可以寫得很精簡,例如:
words.stream()
.map(String::toUpperCase)
.filter(w -> w.length() > 5)
.collect(toList());
但你也可以寫得非常複雜到沒人看得懂:
words.collect(
groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString()))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
這段看似厲害,但實際上維護困難,debug 也困難。
範例:Anagrams 程式三種寫法
傳統 for-loop 寫法(可讀性高)
Map<String, Set<String>> groups = new HashMap<>();
try (Scanner s = new Scanner(dictionary)) {
while (s.hasNext()) {
String word = s.next();
groups.computeIfAbsent(alphabetize(word),
unused -> new TreeSet<>()).add(word);
}
}
搭配後續處理與顯示的部分即可達成需求,清楚、易懂、方便除錯。
過度使用 streams 的寫法(不建議)
Files.lines(dictionary)
.collect(groupingBy(word -> word.chars().sorted()
.collect(StringBuilder::new,
(sb, c) -> sb.append((char) c),
StringBuilder::append).toString()))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.map(group -> group.size() + ": " + group)
.forEach(System.out::println);
這段一次完成整個處理流程,但太複雜,別人幾乎看不懂,自己三週後也會忘記在寫什麼。
折衷、清晰版本(建議)
Files.lines(dictionary)
.collect(groupingBy(word -> alphabetize(word)))
.values().stream()
.filter(group -> group.size() >= minGroupSize)
.forEach(g -> System.out.println(g.size() + ": " + g));
這樣寫保留 streams 的優點,同時兼顧可讀性。
劃重點
哪些場景適合用 streams?
- 要處理資料轉換(map)
- 篩選資料(filter)
- 統計運算(count, sum, average)
- 資料分組(groupingBy)
- 查找符合條件的元素(findFirst, anyMatch)
哪些情況避免使用?
- 需要操作多個中間結果的對照資料(streams 一經 map 就無法回頭)
- 需要 break/continue 或 return 外層方法(lambda 無法做到)
- 涉及錯誤處理與多種異常(checked exception 不好處理)
- 要處理 char[] 時(Java streams 對 char 支援不友善)
真實世界範例:過濾出有效 email 清單
假設你有一串使用者輸入的 email 清單,裡面可能包含空字串或格式錯誤的 email。我們可以用 stream 簡單過濾出有效項目:
List<String> emails = List.of(
"alice@example.com",
"",
"bob@example.com",
"not-an-email",
"carol@example.com"
);
Pattern emailPattern = Pattern.compile("^[\\w.-]+@[\\w.-]+\\.[a-z]{2,}$");
List<String> validEmails = emails.stream()
.filter(email -> emailPattern.matcher(email).matches())
.collect(Collectors.toList());
validEmails.forEach(System.out::println);
這樣寫簡潔而具表達力,但若要在中間處理 p 本身的值,就必須用 pair 或其他 workaround,程式會變複雜。
小結
使用 streams 的指導原則:
- ✔️ 當你需要表達「資料轉換流程」時,用 streams。
- ✔️ 當處理簡潔、單純的資料流轉流程時,用 streams。
- ❌ 當你需要 break、回傳、修改區域變數或處理錯誤時,避開 streams。
- ❌ 當你發現自己寫了兩層以上的 lambda,停下來想一下是否要改寫。
最後提醒,streams 是輔助,不是萬能。在團隊協作中,寫出大家看得懂的程式碼比用複雜的語法還重要。
Read other posts