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