一開始用 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-loop 迴圈
for (int i = 0; i < names.size(); i++) {
System.out.println(names.get(i));
}

// Iterator 迴圈
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}

// enhanced for-loop(本質也是 Iterator)
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");

// Stream forEach
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
// 沒辦法「提前停止」,要用 filter
numbers.stream()
.filter(num -> num <= 3)
.forEach(System.out::println);

// 或 takeWhile(Java 9+)
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
// parallelStream() 自動用多執行緒
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); // 6

內部迭代: 用高階函式組合

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); // 6

// 或用 reduce
int sum = numbers.stream()
.filter(num -> num % 2 == 0)
.reduce(0, Integer::sum);

差異 4:Lambda 參數化

外部迭代: 不支援 lambda

1
2
// 傳統 for-loop 無法用 lambda
// for 關鍵字本身就是語言特性,沒辦法當成高階函式

內部迭代: 完全支援 lambda

1
2
3
4
5
6
7
// Stream 的 forEach 就是函式參數
names.stream()
.forEach(name -> System.out.println("Hello, " + name));

// 可以傳任何 Consumer<T>
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;

// Stream
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);
// 結果:Stream 略慢 10-15%(因為函式呼叫 overhead)

結論: 序列時,外部迭代略快,但差別不大。

平行處理

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;

// parallelStream
startTime = System.nanoTime();
double result = numbers.parallelStream()
.mapToDouble(Math::sqrt)
.sum();
long parallelTime = System.nanoTime() - startTime;

System.out.println("External: " + externalTime); // ~500ms
System.out.println("Parallel Stream: " + parallelTime); // ~150ms

結論: 平行時,Stream 快好幾倍。這就是 Stream 的大招。

何時用哪個

用外部迭代

  1. 需要提前停止迴圈
1
2
3
4
5
6
7
8
9
10
11
for (String name : names) {
if (name.equals("Charlie")) {
break; // 簡單直接
}
System.out.println(name);
}

// Stream 要這樣寫
names.stream()
.takeWhile(name -> !name.equals("Charlie"))
.forEach(System.out::println);
  1. 需要存取索引
1
2
3
4
5
6
7
for (int i = 0; i < names.size(); i++) {
System.out.println(i + ": " + names.get(i)); // 索引很重要
}

// Stream 要借助 IntStream
IntStream.range(0, names.size())
.forEach(i -> System.out.println(i + ": " + names.get(i)));
  1. 需要修改集合內的元素
1
2
3
4
5
for (int i = 0; i < numbers.size(); i++) {
numbers.set(i, numbers.get(i) * 2); // 直接修改
}

// Stream 做不了,因為是 immutable 概念

用內部迭代

  1. 純粹讀取,不修改
  2. 需要轉換(map/filter)
  3. 需要平行處理
  4. 需要聚合操作(sum/count/max)

踩坑筆記

坑 1:parallelStream 的代價

1
2
3
4
5
6
7
8
9
10
11
// 小集合用 parallelStream 反而變慢
List<Integer> small = Arrays.asList(1, 2, 3);
small.parallelStream()
.forEach(System.out::println);
// overhead 超過實際計算,反而比序列慢

// 大集合才值得
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
// forEach 可能被最佳化成平行,導致順序不確定
// 如果順序很重要,別用 forEach
List<String> lines = readLines();

// 錯誤 - forEach 可能亂序
lines.parallelStream().forEach(System.out::println);

// 正確 - forEachOrdered 保證順序
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;
});

// 直到這裡,上面的 map 和 filter 才會執行
pipeline.forEach(System.out::println);

這叫 lazy evaluation,有時是功能,有時是坑。

最後的話

Stream 這種內部迭代方式,改變了 Java 寫程式的風格。對於大部分讀取、轉換的場景,Stream 更簡潔更直觀。但外部迭代還是有它的位置,特別是需要細粒度控制時。兩個都會用,才算是真正理解 Java 集合的人。