整理 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 是輔助,不是萬能。在團隊協作中,寫出大家看得懂的程式碼比用最炫的語法還重要。