CodeGym /課程 /JAVA 25 SELF /Stream API 中的惰性處理(lazy evaluation)

Stream API 中的惰性處理(lazy evaluation)

JAVA 25 SELF
等級 33 , 課堂 1
開放

1. 什麼是惰性處理?

惰性處理,或稱「惰性求值」(lazy evaluation),是一種原則:對資料的操作會被延後,直到結果真的需要時才執行。在 Stream API 的情境下,這表示:即使你寫好了對集合的一連串轉換,Java 也不會立刻執行——它會等到呼叫終端操作時才開始計算整個鏈。

為什麼需要這樣?其一,節省資源:最終用不到的元素根本不會被處理。其二,提升效能——可以建立很長的操作鏈,而不必建立大量的中間集合。最後,還能進行「短路運算」:一旦找到第一個符合條件的元素,後續處理就會停止。

打個比方:想像一位懶懶的服務生。你說:「把菜單拿來,然後咖啡,接著甜點。」他點頭,但什麼也不做……直到你補上一句:「好吧,現在真的去拿吧。」這時他才去執行點單——而且如果甜點已經沒有了,他可能只端來咖啡。串流的惰性處理大致也是這樣運作。

2. 中間與終端操作

中間操作(intermediate):

  • filter
  • map
  • sorted
  • distinct
  • peek(用於除錯)
  • 等等

中間操作會回傳新的 Stream,但不會觸發計算。它們只是在「建立處理計畫」。

終端操作(terminal):

  • collect
  • forEach
  • reduce
  • count
  • findFirstfindAny
  • anyMatchallMatchnoneMatch
  • 等等

只有終端操作會啟動整個鏈的執行。

範例:沒有終端操作就不會有任何動作

List<String> names = List.of("Alisa", "Bob", "Vasya");

names.stream()
     .filter(name -> {
         System.out.println("篩選中 " + name);
         return name.startsWith("A");
     });
// 不會有任何輸出!上面的程式碼只是在 "構建" 鏈。

現在加上一個終端操作:

names.stream()
     .filter(name -> {
         System.out.println("篩選中 " + name);
         return name.startsWith("A");
     })
     .forEach(System.out::println);
// 現在你會在主控台看到輸出!

結果:

篩選中 Alisa
篩選中 Bob
篩選中 Vasya
Alisa

3. 惰性處理的優點

節省資源

惰性處理能避免把時間和記憶體浪費在不需要的元素上。例如,當你只需要找到第一個符合條件的物件時,處理會在第一次匹配後停止。

List<String> names = List.of("Alisa", "Bob", "Vasya", "Anna");

String firstA = names.stream()
    .filter(name -> {
        System.out.println("檢查:" + name);
        return name.startsWith("A");
    })
    .findFirst()
    .orElse("未找到");

System.out.println("結果:" + firstA);

輸出:

檢查:Alisa
結果:Alisa

請注意:其他元素根本不會被檢查!

不需建立中間集合的長鏈

你可以組合多個操作(filtermapsorted 等),而不必在每一步建立集合。

List<String> names = List.of("Alisa", "Bob", "Vasya", "Anna");

List<String> result = names.stream()
    .filter(name -> name.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .toList(); // Java 16+,較早版本 — .collect(Collectors.toList())

短路運算

如果只需知道「元素中是否存在合適的」,其餘元素將不會被檢查:

boolean hasLongName = names.stream()
    .anyMatch(name -> {
        System.out.println("檢查:" + name);
        return name.length() > 10;
    });

// 如果第一個元素就很長 — 其餘的不會被檢查!

4. 範例:惰性處理如何運作

範例 1:沒有終端操作就不會有任何動作

List<Integer> numbers = List.of(1, 2, 3, 4, 5);

numbers.stream()
    .filter(n -> {
        System.out.println("篩選中 " + n);
        return n % 2 == 0;
    });
// 沒有輸出!

範例 2:包含終端操作的鏈

numbers.stream()
    .filter(n -> {
        System.out.println("篩選中 " + n);
        return n % 2 == 0;
    })
    .map(n -> {
        System.out.println("乘以 " + n);
        return n * 10;
    })
    .forEach(System.out::println);

輸出:

篩選中 1
篩選中 2
乘以 2
20
篩選中 3
篩選中 4
乘以 4
40
篩選中 5

重要說明:操作是逐元素執行的:先 filter,再 map,再 forEach——對每個元素依序進行。這並不是兩個獨立的遍歷:「先把所有元素過濾完,再把所有元素轉換完」。

範例 3:使用 peek 來除錯

numbers.stream()
    .filter(n -> n % 2 == 0)
    .peek(n -> System.out.println("通過篩選:" + n))
    .map(n -> n * 10)
    .peek(n -> System.out.println("map 之後:" + n))
    .forEach(System.out::println);

5. 實用細節

避免在串流中使用副作用

惰性可能會捉弄你,如果你預期立即執行的話。在 mapfilterpeek 中進行副作用(寫入檔案、修改外部狀態)可能不會按照你預期的順序執行、可能不會對所有元素執行,甚至在沒有終端操作時完全不會執行。

盡可能把過濾放在前面

盡量把 filter 放在鏈的前段,以更早排除多餘元素、減少後續工作量。

只需要第一個結果?請使用對應的終端操作

若只需要第一個符合條件的元素——請呼叫 findFirstfindAny。這能讓串流在找到結果後立刻停止。

串流不是用來修改原始集合

串流並非用於在原始集合上新增/刪除元素。若要修改集合結構,請使用其他機制。

惰性串流的工作流程視覺化

List<String> words = List.of("cat", "dog", "elephant", "fox", "giraffe");

words.stream()
    .filter(w -> w.length() > 3)
    .map(String::toUpperCase)
    .forEach(System.out::println);

過程如下:

階段 cat dog elephant fox giraffe
filter
map
ELEPHANT GIRAFFE
forEach
列印 列印

表格:Eager 與 Lazy 方法的比較

方法 何時執行處理? 記憶體使用 效能
Eager(貪婪) 在呼叫時立即執行 可能較多 有時較慢
Lazy(惰性) 僅在需要時 最小 通常更快

貪婪方法——例如你手動對集合進行多次遍歷,並建立中間清單。
惰性方法——也就是串流:在需要最終結果之前什麼都不做。

6. 使用惰性串流時的常見錯誤

錯誤 1:期待立即得到結果。 新手常以為呼叫 filtermap 會立刻執行。但沒有終端操作(例如 collectforEach)就什麼也不會發生——於是出現「除錯不起作用」、「沒有任何輸出」的情況。

錯誤 2:在中間操作中加入副作用。map/filter/peek 中進行寫檔、修改外部變數等副作用是個壞習慣。由於惰性與最佳化,這些動作可能不會完整執行、執行順序不如預期,或者在沒有終端操作時根本不會執行。

錯誤 3:忘了呼叫終端操作。 寫了串流鏈但沒有以 collectforEach 等方式收尾。結果就是「一片寂靜」。

錯誤 4:以為所有元素都會被處理。findFirstanyMatch 這類操作會在取得第一個結果時中止管線。其他元素不會被處理——於是讓人困惑「為什麼我的 println 沒有對所有元素執行?」。

錯誤 5:用串流來更改原始集合。 串流不適合用來修改原始集合(新增/刪除元素)。請使用集合的專用方法或迭代器。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION