Java 逆引き集 | Intermediate / Terminal 操作の理解 — パイプライン設計

Java Java
スポンサーリンク

Intermediate / Terminal 操作の理解 — パイプライン設計

ストリームは「中間操作で加工し、終端操作で結果を取り出す」流れで動きます。中間は“つなぐ”、終端は“決着させる”。この違いが分かると、無駄なく安全なパイプラインが設計できます。


ストリームの基本構造

  • 生成: list.stream()IntStream.range(...) などでストリームを作る。
  • 中間操作(Intermediate): 変換・選別・整列など。新しいストリームを返し、遅延評価される。
  • 終端操作(Terminal): 収集・集計・一致判定など。ここで初めて実行され、ストリームは消費される。
List<String> result = names.stream()           // 生成
    .filter(n -> n.length() >= 3)              // 中間: 絞り込み
    .map(String::toUpperCase)                  // 中間: 変換
    .sorted()                                  // 中間: 並べ替え
    .collect(Collectors.toList());             // 終端: 収集
Java

中間操作の整理(よく使うもの)

  • filter: 条件で要素を残す
  • map / flatMap: 変換(flatMap は複数出力の平坦化)
  • peek: デバッグ用に途中観察(副作用前提の処理には使わない)
  • distinct: 重複排除(全体を見てメモリを使う可能性)
  • sorted: ソート(全体を扱うためコスト高)
  • limit / skip: 先頭から切り取り・スキップ
  • mapToInt/Long/Double: プリミティブストリームへ変換(集計が高速)

終端操作の整理(結果を決めるもの)

  • collect: リスト・セット・マップへの収集、集約(Collector)
  • forEach / forEachOrdered: 各要素への処理(副作用は最小限に)
  • reduce: 畳み込み(合計、積、カスタム集約)
  • count / sum / average: 件数・合計・平均
  • min / max: 最小・最大
  • anyMatch / allMatch / noneMatch: 条件一致チェック
  • findFirst / findAny: 最初/どれか1件の取得(短絡評価)

例題で理解するパイプライン設計

例題1: 合格者の名前だけ、アルファベット順で取得

class Student { String name; int score; Student(String n,int s){name=n;score=s;} }

List<Student> students = List.of(
    new Student("Tanaka", 80),
    new Student("Sato", 55),
    new Student("Ito", 90)
);

List<String> passedNames = students.stream()
    .filter(s -> s.score >= 60)       // 中間: 条件抽出
    .map(s -> s.name)                 // 中間: フィールド変換
    .sorted()                         // 中間: 並べ替え
    .collect(Collectors.toList());    // 終端: 収集
Java

例題2: 商品価格の税計算→平均価格

List<Integer> prices = List.of(100, 200, 300);

double avg = prices.stream()
    .mapToInt(p -> (int) Math.round(p * 1.1)) // 中間: 10%加算の整数化
    .average()                                 // 終端: 平均
    .orElse(0.0);
Java

例題3: ログ行から「WARN/ERROR の先頭5件」を抽出して出力

List<String> logs = List.of("INFO x", "WARN a", "ERROR b", "WARN c", "INFO d", "ERROR e");

logs.stream()
    .filter(l -> l.startsWith("WARN") || l.startsWith("ERROR")) // 中間
    .limit(5)                                                   // 中間: 有限化
    .forEach(System.out::println);                              // 終端
Java

設計のコツ(遅延評価とメモリを活かす)

  • 順序(軽い→重い):
    • ラベル: 先に filter で絞る、最後に sorted/distinct を置くと無駄が減る。
  • 短絡評価を使う:
    • ラベル: anyMatch/findFirst は条件に合えば即終了、巨大入力で効く。
  • 中間で collect しない:
    • ラベル: 不要な toList は避け、終端までパイプラインで流す。
  • プリミティブストリームで集計:
    • ラベル: mapToInt/Long/Double → sum/average は高速で低メモリ。
  • peek はデバッグ専用:
    • ラベル: 本番ロジックの副作用は forEach(終端)で最小限に。
  • 有限化の徹底:
    • ラベル: iterate/generate を使うときは limit/takeWhile を必ず入れる。

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

  • 基本形(絞り込み→変換→収集)
var out = list.stream()
    .filter(this::cond)
    .map(this::transform)
    .collect(Collectors.toList());
Java
  • 集計(プリミティブストリーム)
int sum = list.stream()
    .mapToInt(this::toInt)
    .sum();
Java
  • 一致判定で早く終わる
boolean exists = list.stream().anyMatch(this::cond);
Java
  • 最初の一致を取得(なければデフォルト)
String first = list.stream()
    .filter(this::cond)
    .findFirst()
    .orElse("NA");
Java
  • 重い操作は後ろに(並べ替えは最後)
var out = list.stream()
    .filter(this::fastCheck)
    .map(this::mapLight)
    .sorted(this::compareHeavy)
    .collect(Collectors.toList());
Java

落とし穴と回避策

  • 終端操作を忘れて“何も起きない”:
    • 回避: 最後に collect/sum/forEach などを必ず置く。
  • 巨大入力で sorted/distinct が重い:
    • 回避: 先に filter、必要なら並列化か段階的処理を検討。
  • 副作用の混入でバグ化:
    • 回避: 中間操作は純粋関数に。外部書き込みは終端で最小限に。
  • 無限ストリームが走り続ける:
    • 回避: limit/takeWhile を必ず入れて有限化。
  • 中間で toList して再ストリーム化の無駄:
    • 回避: 可能な限り1本のパイプラインで完結させる。

まとめ

  • 中間操作は「つなぐ・変換」、終端操作は「決着・取り出し」。遅延評価を活かすために、軽い操作を前に、重い操作を後ろに配置する。
  • 絞り込み→変換→集約が基本形。短絡評価・プリミティブストリーム・有限化で、メモリと時間を無駄にしないパイプラインを設計できる。
タイトルとURLをコピーしました