Java 逆引き集 | lazy evaluation の理解(中間操作は遅延) — 無駄な処理回避

Java Java
スポンサーリンク

ねらいと前提

Stream の中間操作(filter, map, sorted など)は「遅延評価」です。つまり、終端操作(collect, forEach, count など)が呼ばれるまで実行されません。これを正しく理解すると、無駄な処理をしないパイプライン設計ができ、メモリ・CPU・I/O の無駄を劇的に減らせます。重要ポイントは「終端がドライバー」「短絡で早く止める」「上流で絞る」「副作用を中間に混ぜない」です。


遅延評価の核心(終端が動かす)

終端が初めてパイプラインを“動かす”

中間操作は「説明書の継ぎ足し」でしかありません。終端を呼ぶまで一切走りません。

Stream<String> s = Stream.of(" a ", "b", "   ")
    .filter(x -> !x.isBlank())  // まだ実行されない
    .map(String::trim);          // まだ実行されない

// ここで初めて実行される
List<String> out = s.collect(Collectors.toList()); // ["a", "b"]
Java

この「終端がドライバー」の性質を前提に、余計な材質化(toList など)を避け、必要な終端だけ置きます。

要素は“1件ずつ”段階を通って処理される

実行が始まると、終端が1件ずつ要素を引き出し、filter → map → … を通過して結果に到達します。無駄な全件中間生成は起きず、短絡(limit, findFirst など)にぶつかれば早く止まれます。


無駄を消す設計(短絡と順序)

短絡操作で“早く止める”

limit・findFirst・anyMatch は、必要数に達したら即停止します。重い map/sorted の前に置けば無駄が大幅に減ります。

// 悪い順序(全件ソート後に先頭1件)
String topBad = users.stream()
    .sorted(Comparator.comparingInt(User::score).reversed())
    .map(User::name)
    .findFirst().orElse("NONE");

// 良い順序(先にトップ1へ絞ってから整形)
String topGood = users.stream()
    .sorted(Comparator.comparingInt(User::score).reversed())
    .limit(1)              // ここで短絡
    .map(User::name)
    .findFirst().orElse("NONE");
Java

sorted は高コストなので、limit と組み合わせた“トップ N”は必ず短絡を前段に活用します。

安い絞り込みを前へ、重い処理を後ろへ

filter(安い)→ limit/takeWhile(短絡)→ map(重い)→ sorted/distinct(重い)→ 終端、が基本順序。上流で不要を削れば、後段の重い処理に到達する要素が減り、総コストが下がります。


例題で腹落ちさせる遅延評価

例題1:peek ログで「動いていない」を確認

終端が無いと何も出力されません。終端を足すと、初めて中間が走ります。

Stream.of("A", " ", "B")
    .filter(s -> !s.isBlank())
    .peek(s -> System.out.println("after filter: " + s)); // 何も出ない(終端なし)

long cnt = Stream.of("A", " ", "B")
    .filter(s -> !s.isBlank())
    .peek(s -> System.out.println("after filter: " + s))  // ここで出る
    .count();                                             // 終端

// 出力:
// after filter: A
// after filter: B
Java

例題2:limit の位置で仕事量が激減する

List<String> names = List.of("alice","bob","carol","dan","erin");

// 悪い:全件を重い正規化してから limit
List<String> bad = names.stream()
    .map(this::expensiveNormalize)
    .limit(2)
    .toList();

// 良い:安いフィルタで先に絞り、必要数に達したら止める
List<String> good = names.stream()
    .filter(n -> n.length() >= 3)   // 安い
    .limit(2)                       // ここで停止
    .map(this::expensiveNormalize)  // 必要分だけ
    .toList();
Java

短絡は「不要な後続処理をゼロ」にします。重い処理の前で止めるのが鉄則です。

例題3:I/O ストリームは逐次で“流し切る”

遅延評価は I/O と相性が良く、行を1つずつ処理してすぐ外へ出せます。材質化は不要です。

try (var w = Files.newBufferedWriter(Paths.get("out.txt"))) {
    Files.lines(Paths.get("in.txt"))      // 行を遅延で供給
        .filter(l -> !l.isBlank())
        .map(String::trim)
        .forEach(l -> {                   // 終端:逐次書き出し
            try { w.write(l); w.newLine(); }
            catch (IOException e) { throw new UncheckedIOException(e); }
        });
}
Java

よくある落とし穴と回避

終端なしデバッグ(peek の“無反応”)

peek は終端が無いと動きません。必ず count/collect/forEach などの終端を付けます。並列時は出力順が崩れるため、確認は forEachOrdered を必要箇所に限定します。

中間で副作用を入れる

中間操作に「外部リストへ add」などの副作用を入れると、短絡や並列で壊れます。副作用は終端でのみ。中間は純粋(入力→出力)に徹します。

// NG(副作用):map 内で外部状態を更新
map(x -> { external.add(x); return normalize(x); });

// OK:終端で外部へ流す
forEach(external::add);
Java

複数終端を同じストリームにかける

ストリームは一回きり。複数の終端が必要なら「再生成」か「一度材質化して分岐」。

Supplier<Stream<String>> base = () -> data.stream().filter(this::cond);

// 別々の終端(再生成)
long n = base.get().count();
List<String> top = base.get().limit(100).toList();
Java

遅延評価を活かす設計チートシート

短絡で止めるテンプレート

Optional<T> firstValid = stream
    .filter(this::cheapCheck)
    .map(this::normalize)
    .findFirst(); // 必要が満たされたら停止
Java

I/O を終端に寄せて材質化ゼロ

Files.lines(path)
    .filter(this::cheapCond)
    .map(this::format)
    .forEach(this::writeOut); // 逐次出力でピーク一定
Java

先に絞ってから重い処理

List<R> out = in.stream()
    .filter(this::cheapCond)      // 上流で削る
    .limit(1000)                  // 短絡で止める
    .map(this::expensiveTransform)
    .toList();
Java

同じ上流を複数評価(Supplier 再生成)

Supplier<Stream<U>> src = () -> users.stream().filter(User::isActive);

long count = src.get().count();
List<String> names = src.get().map(User::name).toList();
Java

まとめ

中間操作は「遅延」し、終端が呼ばれたときに初めて動きます。だからこそ、安い絞り込みを前段に、短絡で早く止め、重い処理は必要最小限だけ通す。副作用は終端に限定し、I/O は逐次へ。複数終端は再生成か小さな材質化で分岐。遅延評価の本質(終端がドライバー)を掴めば、無駄な処理を根こそぎ避け、読みやすく速いストリーム設計が自然にできるようになります。

タイトルとURLをコピーしました