1. 問題: 通常の Stream が常に効率的とは限らない理由
Java で数値コレクションを通常のストリーム(Stream<Integer>、Stream<Double>)で扱うと、内部ではプリミティブ値がラッパーオブジェクト(Integer、Double など)に「ボクシング(boxing)」され、逆に「アンボクシング(unboxing)」されます。これは便利ですが、常に高効率とは限りません。
- 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);
ここでは各要素はプリミティブの int ではなく、Integer オブジェクトです。
2. プリミティブストリーム: IntStream, LongStream, DoubleStream
余分な boxing/unboxing を避けるために、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) // 0..99 のランダムな int を5個
例: 配列要素の合計
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);
}
2つ目の方法は、Integer オブジェクトの生成が絶えず発生するため遅くなります。
結論:
性能が重要なら、プリミティブストリームを使いましょう。
6. プリミティブストリームが有効なとき/無理に使わないほうがよいとき
次の場合はプリミティブストリームを使うと良い:
- 大きな配列/コレクションの数値を扱う。
- 合計・平均・最小・最大を素早く計算したい。
- リアルタイム処理など、1ミリ秒単位でも重要なコードを書く。
次の場合はこだわらなくてよい:
- コレクションが小さい(要素が数個〜数十個)。
- 型変換のせいでコードが複雑になってしまう。
- 性能が重要ではない(例: ユーザー入力の処理)。
例:
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