一開始用 Stream 時,就是 forEach 走天下。後來才發現 Stream forEach 和傳統 Iterator 根本是兩套邏輯,搞混的話效能會爆。
概念差異
傳統迭代:外部迭代(External Iteration)
你決定怎麼走訪:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
for (int i = 0; i < names.size(); i++) { System.out.println(names.get(i)); }
Iterator<String> iterator = names.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); }
for (String name : names) { System.out.println(name); }
|
特點: 你主動控制「什麼時候」取下一個元素。
Stream 迭代:內部迭代(Internal Iteration)
JDK 決定怎麼走訪:
1 2 3 4 5 6 7 8
| List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream().forEach(System.out::println);
names.stream() .forEach(name -> System.out.println(name));
|
特點: 你只提供「對每個元素做什麼」的邏輯,迭代方式由 JDK 決定。
主要差異
差異 1:迭代控制權
外部迭代: 你掌控流程
1 2 3 4 5 6 7 8 9 10 11
| List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Iterator<Integer> iter = numbers.iterator(); while (iter.hasNext()) { int num = iter.next(); if (num > 3) { break; } System.out.println(num); }
|
內部迭代: JDK 掌控流程
1 2 3 4 5 6 7 8 9
| numbers.stream() .filter(num -> num <= 3) .forEach(System.out::println);
numbers.stream() .takeWhile(num -> num <= 3) .forEach(System.out::println);
|
差異 2:平行處理
外部迭代: 只能序列
1 2 3 4
| for (String name : largeList) { process(name); }
|
內部迭代: 支援平行處理
1 2 3 4 5 6 7 8
| largeList.stream() .parallel() .forEach(name -> process(name));
largeList.parallelStream() .forEach(name -> process(name));
|
這就是為什麼 Stream 這麼紅。一行程式碼,從序列變平行。
差異 3:Aggregate Operation
外部迭代: 要自己組裝邏輯
1 2 3 4 5 6 7 8 9 10
| List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5); int sum = 0;
for (Integer num : numbers) { if (num % 2 == 0) { sum += num; } } System.out.println(sum);
|
內部迭代: 用高階函式組合
1 2 3 4 5 6 7 8 9 10
| int sum = numbers.stream() .filter(num -> num % 2 == 0) .mapToInt(Integer::intValue) .sum(); System.out.println(sum);
int sum = numbers.stream() .filter(num -> num % 2 == 0) .reduce(0, Integer::sum);
|
差異 4:Lambda 參數化
外部迭代: 不支援 lambda
內部迭代: 完全支援 lambda
1 2 3 4 5 6 7
| names.stream() .forEach(name -> System.out.println("Hello, " + name));
Consumer<String> greeter = name -> System.out.println("Hi " + name); names.stream().forEach(greeter);
|
代碼對比
場景:取平方後過濾 > 10,全部打印
外部迭代:
1 2 3 4 5 6 7 8
| List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
for (Integer num : numbers) { int squared = num * num; if (squared > 10) { System.out.println(squared); } }
|
內部迭代(序列):
1 2 3 4
| numbers.stream() .map(num -> num * num) .filter(squared -> squared > 10) .forEach(System.out::println);
|
內部迭代(平行):
1 2 3 4
| numbers.parallelStream() .map(num -> num * num) .filter(squared -> squared > 10) .forEach(System.out::println);
|
只改一個詞 parallelStream,就變成多執行緒。外部迭代不可能這麼簡單。
效能對比
序列處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| List<Integer> numbers = generateList(1000000);
long startTime = System.nanoTime(); for (Integer num : numbers) { if (num % 2 == 0) { Math.sqrt(num); } } long externalTime = System.nanoTime() - startTime;
startTime = System.nanoTime(); numbers.stream() .filter(num -> num % 2 == 0) .forEach(num -> Math.sqrt(num)); long streamTime = System.nanoTime() - startTime;
System.out.println("External: " + externalTime); System.out.println("Stream: " + streamTime);
|
結論: 序列時,外部迭代略快,但差別不大。
平行處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| List<Integer> numbers = generateList(10000000);
long startTime = System.nanoTime(); int result = 0; for (Integer num : numbers) { result += Math.sqrt(num); } long externalTime = System.nanoTime() - startTime;
startTime = System.nanoTime(); double result = numbers.parallelStream() .mapToDouble(Math::sqrt) .sum(); long parallelTime = System.nanoTime() - startTime;
System.out.println("External: " + externalTime); System.out.println("Parallel Stream: " + parallelTime);
|
結論: 平行時,Stream 快好幾倍。這就是 Stream 的大招。
何時用哪個
用外部迭代
- 需要提前停止迴圈
1 2 3 4 5 6 7 8 9 10 11
| for (String name : names) { if (name.equals("Charlie")) { break; } System.out.println(name); }
names.stream() .takeWhile(name -> !name.equals("Charlie")) .forEach(System.out::println);
|
- 需要存取索引
1 2 3 4 5 6 7
| for (int i = 0; i < names.size(); i++) { System.out.println(i + ": " + names.get(i)); }
IntStream.range(0, names.size()) .forEach(i -> System.out.println(i + ": " + names.get(i)));
|
- 需要修改集合內的元素
1 2 3 4 5
| for (int i = 0; i < numbers.size(); i++) { numbers.set(i, numbers.get(i) * 2); }
|
用內部迭代
- 純粹讀取,不修改
- 需要轉換(map/filter)
- 需要平行處理
- 需要聚合操作(sum/count/max)
踩坑筆記
坑 1:parallelStream 的代價
1 2 3 4 5 6 7 8 9 10 11
| List<Integer> small = Arrays.asList(1, 2, 3); small.parallelStream() .forEach(System.out::println);
List<Integer> huge = generateMillionElements(); huge.parallelStream() .filter(heavyComputation) .forEach(System.out::println);
|
一般來說,集合數 > 10000 且操作複雜,parallelStream 才划算。
坑 2:forEach 和 for-loop 的語意差異
1 2 3 4 5 6 7 8 9
|
List<String> lines = readLines();
lines.parallelStream().forEach(System.out::println);
lines.parallelStream().forEachOrdered(System.out::println);
|
坑 3:Stream 會延遲執行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| List<Integer> numbers = Arrays.asList(1, 2, 3);
var pipeline = numbers.stream() .map(n -> { System.out.println("Mapping: " + n); return n * 2; }) .filter(n -> { System.out.println("Filtering: " + n); return n > 2; });
pipeline.forEach(System.out::println);
|
這叫 lazy evaluation,有時是功能,有時是坑。
最後的話
Stream 這種內部迭代方式,改變了 Java 寫程式的風格。對於大部分讀取、轉換的場景,Stream 更簡潔更直觀。但外部迭代還是有它的位置,特別是需要細粒度控制時。兩個都會用,才算是真正理解 Java 集合的人。
你的鼓勵將被轉換為我明天繼續加班的動力(真的)。 ❤️