1. 問題:為什麼一般的 Stream 並非總是高效
當你在 Java 中處理數值集合並使用一般串流(Stream<Integer>、Stream<Double>)時,底層會發生「裝箱」(boxing)與「拆箱」(unboxing):把原始型別包成包裝類別(Integer、Double 等等)或反向還原。這很方便,但不一定高效:
- Boxing — 將原始型別(int)轉成物件(Integer)。
- Unboxing — 相反的操作:由物件還原為原始型別。
問題:
Boxing/unboxing 會增加額外的操作與記憶體耗用。在「熱點」(被頻繁呼叫的)程式碼路徑上,這可能導致顯著的效能下降,尤其當你處理大型數值陣列時。
範例:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().map(x -> x * 2).reduce(0, Integer::sum);
這裡每個數字都是 Integer 物件,而非原始型別 int。
2. 原始型別串流:IntStream, LongStream, DoubleStream
為了避免多餘的裝箱/拆箱,Java 提供了原始型別串流:
- IntStream — 用於 int
- LongStream — 用於 long
- DoubleStream — 用於 double
它們僅處理原始型別,不會產生多餘的包裝物件。
如何建立原始型別串流?
來自陣列:
int[] arr = {1, 2, 3, 4, 5};
IntStream s = Arrays.stream(arr);
使用 range/rangeClosed:
IntStream.range(0, 10) // 0..9
IntStream.rangeClosed(1, 5) // 1..5(含端點)
產生隨機數:
new Random().ints(5, 0, 100) // 5 個隨機 int,介於 0 到 99
範例:陣列元素求和
int[] arr = {1, 2, 3, 4, 5};
int sum = Arrays.stream(arr).sum(); // 15
3. 在 Stream<T> 與原始型別串流之間轉換
有時你手上是一般串流,有時則是原始型別串流。可透過以下特殊方法進行轉換:
- mapToInt、mapToLong、mapToDouble — 將一般串流轉成原始型別串流。
- boxed() — 將原始型別串流轉回物件串流。
範例:
List<String> words = List.of("Java", "Stream", "API");
IntStream lengths = words.stream().mapToInt(String::length);
lengths.forEach(System.out::println); // 4 6 3
反向:
IntStream ints = IntStream.range(1, 5);
Stream<Integer> boxed = ints.boxed();
注意:
boxed() 是反向操作,它會再次建立包裝物件(Integer、Double 等),若你在乎極致效能——請在「熱點」程式碼中避免使用。
4. 加總與摘要:sum, average, min, max, summaryStatistics
原始型別串流提供了便捷的彙總方法:
- sum() — 所有元素的總和
- average() — 平均值(回傳 OptionalDouble)
- min()、max() — 最小值與最大值(OptionalInt、OptionalLong、OptionalDouble)
- summaryStatistics() — 回傳包含完整統計的物件(總和、平均、最小值、最大值、數量)
範例:
int[] arr = {1, 2, 3, 4, 5};
IntSummaryStatistics stats = Arrays.stream(arr).summaryStatistics();
System.out.println(stats.getSum()); // 15
System.out.println(stats.getAverage()); // 3.0
System.out.println(stats.getMin()); // 1
System.out.println(stats.getMax()); // 5
System.out.println(stats.getCount()); // 5
與 Collectors 的比較:
對一般串流可以使用 Collectors.summarizingInt,但這通常比原始型別串流的方法效率更低:
List<Integer> nums = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = nums.stream().collect(Collectors.summarizingInt(x -> x));
5. 避免自動裝箱:哪些情況重要、如何量測
哪些情況特別關鍵?
- 在迴圈與串流中處理大量數值(例如處理陣列、統計、數學運算、資料解析)。
- 在「熱點」位置——被頻繁呼叫且影響整體效能的程式碼。
為什麼重要?
- 每次裝箱都會建立新的包裝物件(Integer、Double 等)。
- 這會加重垃圾回收器(GC)的負擔。
- 在微型效能測試中,差距可能達數倍!
如何量測?
要精確比較,使用框架 JMH(Java Microbenchmark Harness)。它能公平地比較有無裝箱的程式效能。
範例(偽代碼):
@Benchmark
public int sumIntStream() {
return IntStream.range(0, 1_000_000).sum();
}
@Benchmark
public int sumStreamInteger() {
return Stream.iterate(0, n -> n + 1).limit(1_000_000).reduce(0, Integer::sum);
}
第二個版本會因為不斷建立 Integer 物件而較慢。
結論:
若你在乎效能——請使用原始型別串流!
6. 什麼時候原始型別串流真的有幫助,什麼時候不必複雜化
以下情況請使用原始型別串流:
- 你在處理大型數值陣列/集合。
- 你需要快速計算總和、平均、最小、最大。
- 你在撰寫對每毫秒都很敏感的程式(例如即時資料處理)。
以下情況可以不必執著:
- 集合很小(只有幾個到十數個元素)。
- 為了在型別間轉換而讓程式變得過度複雜。
- 效能不是關鍵(例如處理使用者輸入)。
範例:
List<Integer> smallList = List.of(1, 2, 3);
int sum = smallList.stream().mapToInt(x -> x).sum(); // 可以,但用一般的 reduce 也行
建議:不要為了微乎其微的優化把程式寫成「叢林」。在真正值得的地方才使用原始型別串流。
7. OptionalInt, OptionalDouble: 安全地取得結果
原始型別串流的 min()、max()、average() 並非直接回傳數值,而是回傳「包裝」——OptionalInt、OptionalDouble 等。這是為了能安全地處理空串流(例如陣列為空)。
範例:
int[] arr = {};
OptionalInt min = Arrays.stream(arr).min();
if (min.isPresent()) {
System.out.println("最小值: " + min.getAsInt());
} else {
System.out.println("陣列為空!");
}
與一般 Optional 的對比:
- OptionalInt — 用於 int
- OptionalDouble — 用於 double
- OptionalLong — 用於 long
為什麼不直接回傳 0?
因為 0 可能是有效值,而空串流是不同的情況。
average 的範例:
double[] arr = {};
OptionalDouble avg = Arrays.stream(arr).average();
double result = avg.orElse(Double.NaN); // 若為空 — 回傳 NaN
8. 實作:原始型別串流的使用範例
範例 1:1 到 1000 的平方和
int sum = IntStream.rangeClosed(1, 1000)
.map(x -> x * x)
.sum();
System.out.println(sum);
範例 2:過濾並計算偶數的個數
int[] arr = {1, 2, 3, 4, 5, 6};
long count = Arrays.stream(arr)
.filter(x -> x % 2 == 0)
.count();
System.out.println("偶數個數: " + count);
範例 3:與一般的 Stream<Integer> 比較
List<Integer> list = IntStream.range(0, 1_000_000)
.boxed()
.collect(Collectors.toList());
long t1 = System.currentTimeMillis();
int sum1 = list.stream().mapToInt(x -> x).sum();
long t2 = System.currentTimeMillis();
System.out.println("Stream<Integer>: " + (t2 - t1) + " ms");
t1 = System.currentTimeMillis();
int sum2 = IntStream.range(0, 1_000_000).sum();
t2 = System.currentTimeMillis();
System.out.println("IntStream: " + (t2 - t1) + " ms");
在大量資料上,差異會十分明顯。
9. 使用原始型別串流時的常見錯誤
錯誤 1:在型別轉換時忽略了裝箱。
如果你使用 boxed(),請記得它會再次建立包裝物件。非必要請避免。
錯誤 2:把原始型別串流用在物件上。
IntStream 只能處理 int,而非物件。若要處理物件——請使用一般的 Stream<T>。
錯誤 3:忽略 OptionalInt/OptionalDouble。
當你呼叫 min()、max()、average() 時,務必先確認結果存在(isPresent()),否則可能拋出例外。
錯誤 4:在 Stream<T> 與 IntStream 之間做過度複雜的轉換。
如果程式因不停地 mapToInt() → boxed() → mapToDouble() 而變得難以閱讀,也許該簡化邏輯。
錯誤 5:期待在小型集合上出現「魔法般」的加速。
對於小型列表,Stream 與 IntStream 的差別極小。不要為了微小的效能節省而讓程式變複雜。
GO TO FULL VERSION