Java 逆引き集 | Iterator vs Stream の使い分け(遅延評価・メモリ) — 性能設計

Java Java
スポンサーリンク

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 を選ぶと、シンプルで速いコードになりやすい。
タイトルとURLをコピーしました