Java 逆引き集 | ストリームの短絡(short-circuit)操作 — パフォーマンス最適化

Java Java
スポンサーリンク

ストリームの短絡(short-circuit)操作 — パフォーマンス最適化

短絡操作は「必要な分だけ処理したら早めに打ち切る」ための仕組みです。無駄な計算や I/O を避け、スループットを上げる実戦テクです。代表例は anyMatch/findFirst/limit と、Java 9 の takeWhile/dropWhile。どこで「止めるか」を意識できると、ストリームは一気に速くなります。


短絡の基本と種類

  • 短絡終端操作:
    ラベル: 条件が満たされた瞬間に処理を終了する。例:anyMatch、noneMatch、allMatch、findFirst、findAny。
  • 短絡中間操作:
    ラベル: 流れる要素数を途中で打ち止める。例:limit、takeWhile(Java 9+)。
  • ポイント: 「上流で絞る」「早く止める」を組み合わせると効果が最大。filter を前段に、短絡操作を後段に置くのが基本。

代表的な短絡操作(コードで理解)

anyMatch / noneMatch / allMatch(終端で打ち切り)

List<String> words = List.of("alpha","beta","gamma","delta");

// 1件でも条件を満たせば終了(anyMatch)
boolean hasLong = words.stream().anyMatch(w -> w.length() >= 5);

// 全件が条件を満たさないか(noneMatch)
boolean noneStartZ = words.stream().noneMatch(w -> w.startsWith("z"));

// 全件が条件を満たすか(allMatch)
boolean allLower = words.stream().allMatch(w -> w.equals(w.toLowerCase()));
Java
  • 効果: 条件成立/不成立が確定した時点で残りの要素を処理しない。

findFirst / findAny(1件見つけたら終了)

List<Integer> nums = List.of(3,7,10,12,15);

// 最初の偶数(順序保証あり)
int firstEven = nums.stream()
    .filter(n -> n % 2 == 0)
    .findFirst()
    .orElse(-1);

// どれかひとつ(並列で効率的、順序保証なし)
int anyEven = nums.parallelStream()
    .filter(n -> n % 2 == 0)
    .findAny()
    .orElse(-1);
Java
  • 効果: 該当要素を1件見つけたら打ち切り。並列では findAny が特に効く。

limit(中間で流量を抑える)

List<Integer> first10Squares = 
    Stream.iterate(1, n -> n + 1)  // 無限に生成
          .map(n -> n * n)
          .limit(10)               // 10件で打ち切り
          .toList();
Java
  • 効果: 無限ストリームや巨大データから必要件数だけ抜き取って終了。

takeWhile / dropWhile(Java 9+)

List<Integer> nums = List.of(1,2,3,4,9,1,2);

// 先頭から条件が崩れるまで取り続ける(降順になったら止める等)
List<Integer> prefix = nums.stream()
    .takeWhile(n -> n < 5)
    .toList(); // [1,2,3,4]

// 条件が崩れるまでを捨て、そこから先を流す
List<Integer> rest = nums.stream()
    .dropWhile(n -> n < 5)
    .toList(); // [9,1,2]
Java
  • 効果: 先頭から連続する条件区間だけ処理。ソート済みや前提があるシーケンスで強い。

実務で効く最適化パターン

  • フィルタを前に、短絡を後ろに:
    ラベル: まず filter で対象を狭めて、最後に anyMatch/findFirst で打ち切ると最短で終わる。
  • ソートや重い map の前に limit/takeWhile:
    ラベル: 全件を高コスト処理する前に件数を絞る。ランキング上位 N 件などは先に limit。
  • 並列なら findAny を選ぶ:
    ラベル: findFirst は順序維持のために遅くなる。順序不要なら findAny の方が終了が早い。
  • 生成側から短絡(iterate + limit / takeWhile):
    ラベル: 無限生成は必ず短絡。終端条件を持つ iterate(Java 9)なら takeWhile 相当を内包できる。

例題で短絡の効果を体感

例題1: ログから初めの ERROR を高速抽出

List<String> logs = List.of("INFO start","WARN retry","ERROR failed","ERROR timeout");

String firstError = logs.stream()
    .filter(l -> l.startsWith("ERROR")) // 先に絞る
    .findFirst()                        // 見つけたら即終了
    .orElse("No error");

System.out.println(firstError); // ERROR failed
Java
  • ポイント: 条件に合わない行は早期に捨て、最初の一致で打ち切る。

例題2: 大量データで上位 N 件の重い処理だけ実行

List<String> users = /* 100万件 */;
List<String> top100Profiles = users.stream()
    .sorted()                     // 本来重いので注意
    .limit(100)                   // 100件だけ欲しい
    .map(user -> expensiveLoadProfile(user)) // ここを最小化
    .toList();
Java
  • ポイント: 可能ならソート前にスコアで選抜して limit、またはデータソース側で上位 N を取得してから処理する方がより速い。

例題3: 整列済み数列から閾値未満だけ処理(takeWhile)

List<Integer> sorted = List.of(1,2,3,4,5,6,7,8,9); // 昇順前提
int sumUnder5 = sorted.stream()
    .takeWhile(n -> n < 5) // 5以上が出たら終了
    .mapToInt(Integer::intValue)
    .sum();
System.out.println(sumUnder5); // 10
Java
  • ポイント: 昇順前提があると takeWhile は強力。前提がない場合は効果が薄い。

テンプレート集(そのまま使える)

  • 最初の一致を素早く取る
T value = stream.filter(cond).findFirst().orElse(defaultVal);
Java
  • 並列でどれかひとつ(順序不要)
T value = stream.parallel().filter(cond).findAny().orElse(defaultVal);
Java
  • 無限生成から N 件だけ
Stream.iterate(seed, f).limit(n).forEach(this::use);
Java
  • 条件が崩れるまで取り続ける(Java 9+)
List<T> prefix = stream.takeWhile(cond).toList();
Java
  • 大量データの前処理最適化
List<R> topN = source.stream()
    .filter(cheapCond)
    .limit(n)
    .map(this::expensive)
    .toList();
Java

落とし穴と回避策

  • 順序の前提: findFirst は順序に依存。並列で効率を重視するなら findAny、順序が必要なら直列や forEachOrdered を検討。
  • takeWhile の前提破綻: ソートや「条件が連続」前提がなければ期待通りに止まらない。整列や前処理で前提を作るか、filter+limit へ切り替える。
  • 重い前段処理の後に短絡: 高コストの map/sorted を先に行うと短絡の恩恵が小さくなる。可能なら「安い絞り込み→短絡→重い処理」。
  • 副作用の混入: 短絡は「早く止まる」ので、外部状態更新が中途半端で終わると破綻する。集約は reduce/collect で純粋に。
  • 空ストリームの扱い: findX/match 系は empty を返す可能性がある。orElse/ifPresent で安全に扱う。

まとめ

  • 短絡は「早く止める」ことで無駄を削る最強の武器。終端系(anyMatch/findFirst)と中間系(limit/takeWhile)を、安い絞り込みと組み合わせて使うと効果的。
  • 順序と前提(整列・連続性)を意識し、重い処理の前に短絡を置く。並列では findAny を活用。
  • 実務では「上流で絞る→短絡→必要なら重い処理」の順でパイプラインを設計すると、体感で速くなるはずです。

👉 練習課題: 大量のユーザーイベントから「ERROR かつ重要フラグ」の最初の1件を最短で取得するパイプラインを、直列版と並列版で書き、filter の順番や findFirst/findAny の違いで実行時間がどう変わるかを比較してください。

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