ねらいと前提
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");
Javasorted は高コストなので、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(); // 必要が満たされたら停止
JavaI/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 は逐次へ。複数終端は再生成か小さな材質化で分岐。遅延評価の本質(終端がドライバー)を掴めば、無駄な処理を根こそぎ避け、読みやすく速いストリーム設計が自然にできるようになります。
