Iterator vs Stream の使い分け(遅延評価・メモリ) — 性能設計
「大量データをどう処理するか」は設計の肝。Iterator は手続き的で軽量、Stream は宣言的で強力(遅延評価・合成・集約)。どちらにも得意分野があり、メモリ・可読性・並列化・副作用の扱いで選び分けるのが実務的です。
概要と向き不向き
- Iterator(手続き・逐次):
- 強み: 軽量・明示的制御・メモリ最小(1要素ずつ前進)。I/Oや巨大入力を「読みながら処理」に最適。
- 弱み: 集約・変換の表現が冗長。並列化や複雑な合成が書きづらい。
- Stream(宣言・遅延評価):
- 強み: filter/map/reduce の合成、遅延評価で不要計算を避ける、終端で一気に実行。並列化や Collector に強い。
- 弱み: 外部副作用に弱い(誤用しがち)、小さな処理ではオーバーヘッド、無限/巨大ストリームの有限化忘れが事故要因。
メモリと遅延評価の観点
- 逐次処理でメモリを抑える:
- Iterator: 1件ずつ読み進めるため常に低メモリ。ファイル/DBカーソル相性良。
- Stream: 遅延評価により中間リストを作らず処理可能(toList などの終端で集約しない限り)。Files.lines や IntStream.generate と組み合わせれば「必要な分だけ」消費。
- 中間データの扱い:
- Iterator: 自分でバッファを持たなければ中間なし。
- Stream: 中間操作は基本遅延。だが一部操作(sorted/distinct)や誤って collect → 再処理でメモリ増大に注意。
- 有限化の設計:
- Iterator: hasNext を終了条件に書く。
- Stream: limit/takeWhile で必ず止めどころを設計。終端操作で初めて実行される。
典型シナリオ別の選び方
- ファイル/DBをチャンクで処理: Iterator(BufferedReader.readLine など)で読みながら処理、必要なら自前でチャンク化。Stream でも Files.lines + forEachChunk のように実現可能。
- フィルタ・変換・集約を端的に書きたい: Stream。map/filter/distinct/sorted/summarizing などで宣言的に。
- 外部副作用が多い(ロギング、ネットワーク呼び出し): Iterator の方が制御しやすい。Stream は副作用最小の設計が前提。
- 並列化したい(CPU計算が重い): Stream.parallel が手軽。ただし副作用排除・Collector 並列対応が前提。
- きめ細かい中断や再開が必要: Iterator。ループ途中の break/continue、リトライなどが直感的。
コード例(対比で理解)
1) CSV を逐次フィルタして合計(Iterator)
import java.io.*;
import java.util.*;
public class SumByIterator {
public static void main(String[] args) throws Exception {
try (BufferedReader br = new BufferedReader(new FileReader("data.csv"))) {
String line;
long sum = 0;
while ((line = br.readLine()) != null) {
String[] cols = line.split(",");
int val = Integer.parseInt(cols[2]);
if (val >= 100) sum += val; // 条件合計
}
System.out.println(sum);
}
}
}
Java- ポイント: 一行ずつ処理しメモリ最小。制御が明示的。
2) 同じ処理を Stream で宣言的に
import java.nio.file.*;
import java.util.stream.*;
public class SumByStream {
public static void main(String[] args) throws Exception {
long sum = Files.lines(Path.of("data.csv"))
.map(l -> l.split(","))
.mapToInt(a -> Integer.parseInt(a[2]))
.filter(v -> v >= 100)
.asLongStream()
.sum();
System.out.println(sum);
}
}
Java- ポイント: パイプラインで簡潔。遅延評価で不要行は直ちに捨てられる。
3) 重い計算は並列 Stream が効果的(副作用なし)
import java.util.*;
import java.util.stream.*;
public class ParallelCompute {
public static void main(String[] args) {
List<Integer> data = IntStream.range(0, 1_000_000).boxed().toList();
long res = data.parallelStream()
.mapToLong(ParallelCompute::heavy)
.sum();
System.out.println(res);
}
static long heavy(int x) {
long r = 0;
for (int i = 0; i < 200; i++) r += (x * 31L + i) % 97;
return r;
}
}
Java- ポイント: 副作用なし+終端が集約なら並列の恩恵を受けやすい。
4) 巨大入力のチャンク処理(Iterator 駆動)
import java.util.*;
class Chunks {
static <T> List<T> nextChunk(Iterator<T> it, int size) {
List<T> buf = new ArrayList<>(size);
for (int i = 0; i < size && it.hasNext(); i++) buf.add(it.next());
return buf;
}
}
Java- ポイント: メモリ上限に合わせて小分けに。I/Oと相性が良い。
実務のコツ
- 外部副作用はループ側で: Stream は純粋関数前提で設計し、I/O副作用は終端近くで最小化。Iterator なら副作用を明示的に扱える。
- 中間集約を避けて遅延を生かす: 不要な collect/toList はしない。必要なら小さいチャンクごとに collect → 処理 → 破棄。
- sorted/distinct はメモリに載る: これらは全体を見て処理するため、巨大入力では要注意。Comparator のコストにも配慮。
- 並列は計測して判断: 小さなデータや軽い処理は直列が速いことが多い。ForkJoinPool のスレッドとスレッド安全性を意識。
- 例外・中断の扱い: Stream の途中で「細かく抜ける」のは不得手。複雑な制御が必要なら Iterator/for で書く。
- 境界の明示: 無限/大量ストリームは limit/takeWhile を必ず入れて有限化。ファイル/ネットワークは try-with-resources で確実にクローズ。
テンプレート集(そのまま使える形)
- Iterator 逐次処理(最小メモリ)
for (Iterator<T> it = source.iterator(); it.hasNext(); ) {
T e = it.next();
if (cond(e)) handle(e);
}
Java- Stream 宣言的処理(フィルタ→変換→集約)
var result = stream
.filter(this::cond)
.map(this::transform)
.collect(java.util.stream.Collectors.toList());
Java- Stream の有限化(安全)
stream.takeWhile(this::cond).forEach(this::consume);
// または
stream.limit(N).forEach(this::consume);
Java- 並列 Stream(副作用なし+Collector対応)
var res = list.parallelStream()
.map(this::expensive)
.collect(java.util.stream.Collectors.toList());
Java- チャンク処理(Iterator 駆動)
List<T> chunk;
while (!(chunk = nextChunk(it, CHUNK)).isEmpty()) {
processChunk(chunk);
}
Javaまとめ
- Iterator は「逐次・副作用・制御重視」、Stream は「宣言・遅延・合成・並列化」。
- メモリ観点では両者とも低メモリ運用が可能だが、Stream は中間集約の使い方次第で増える。
- I/Oや複雑な制御は Iterator、データ変換・集約・並列計算は Stream を選ぶと、シンプルで速いコードになりやすい。
