中間操作と終端操作を一言でいうと
Stream API の「中間操作」と「終端操作」は、
“流れをつくる操作(中間)” と “流れを終わらせて結果を取り出す操作(終端)”
という役割分担になっています。
この違いを理解すると、Stream の動きが一気にクリアになります。
特に重要なのは、
- 中間操作は「Stream を返す」のでつなげられる
- 終端操作は「Stream を返さない」のでそこで終わる
- 中間操作は“遅延評価”で、終端操作が呼ばれるまで実行されない
という 3 点です。
ここを丁寧に噛み砕いていきます。
中間操作とは何か
Stream を別の Stream に変換する“途中のステップ”
中間操作(intermediate operation)は、
「ストリームを受け取り、別のストリームを返す操作」
です。
代表的な中間操作は次のようなものです。
filter(条件に合うものだけ残す)map(別の型に変換する)sorted(並び替える)distinct(重複を取り除く)limit/skip(一部だけ取る・飛ばす)peek(途中で値を覗く)
中間操作の特徴は、
「Stream を返すので、何個でもつなげられる」
という点です。
names.stream()
.filter(s -> s.length() >= 4)
.map(String::toUpperCase)
.sorted();
Javaここまでではまだ“流れを宣言しただけ”で、実行はされていません。
中間操作は“遅延評価”である
中間操作は、呼んだ瞬間には実行されません。
names.stream()
.filter(s -> {
System.out.println("filter: " + s);
return s.length() >= 4;
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
});
// ここでは何も出力されない
Java実際の処理は、終端操作が呼ばれた瞬間に初めて動きます。
この「遅延評価」が Stream の効率性の源です。
終端操作とは何か
Stream の流れを“終わらせて”結果を取り出す操作
終端操作(terminal operation)は、
「ストリームを消費して、結果を返す操作」
です。
代表的な終端操作は次のようなものです。
collect(リストなどに集める)forEach(1 件ずつ処理する)count(件数を数える)findFirst/findAny(1 件取り出す)anyMatch/allMatch/noneMatch(条件判定)reduce(畳み込み)
終端操作の特徴は、
「Stream を返さない」
という点です。
long count = names.stream()
.filter(s -> s.length() >= 4)
.count(); // ここでパイプラインが終了
Java終端操作を呼んだ瞬間に、
それまで積み重ねてきた中間操作が“まとめて実行”されます。
中間操作と終端操作の流れを例で理解する
例:名前リストから「4 文字以上の名前の長さ」を集める
import java.util.List;
import java.util.stream.Collectors;
public class Example {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie", "Ann");
List<Integer> result =
names.stream() // Stream<String>
.filter(s -> s.length() >= 4) // Stream<String>
.map(s -> s.length()) // Stream<Integer>
.collect(Collectors.toList()); // List<Integer>
System.out.println(result); // [5, 7]
}
}
Javaこのパイプラインを“処理の流れ”として読むとこうなります。
- ソース:
names.stream()
→ 「名前の流れを作る」 - 中間:
filter
→ 「4 文字以上だけ残す」 - 中間:
map
→ 「名前 → 長さ に変換する」 - 終端:
collect
→ 「結果をリストに集める」
このように、Stream は「処理のステップ」をそのままコードに落とし込めます。
中間操作の実行タイミングを深掘りする
終端操作が呼ばれるまで“実行されない”理由
Stream は「1 要素ずつパイプラインを通す」仕組みになっています。
例えば:
names.stream()
.filter(...)
.map(...)
.count();
Javaこのときの実行順序は、
- 1 件目を
filter→map→countに流す - 2 件目を
filter→map→countに流す - 3 件目を
filter→map→countに流す
という形で、「横方向に」処理されます。
これにより、
- 無駄な処理をしない
- 途中で条件が満たされたらすぐ終わる(
findFirstなど) - 大量データでも効率的
というメリットが生まれます。
中間操作と終端操作の違いを“型”で見抜く
中間操作は「Stream を返す」
終端操作は「Stream を返さない」
これが最も明確な違いです。
中間操作の例:
Stream<T> filter(Predicate<? super T> predicate)
Stream<R> map(Function<? super T, ? extends R> mapper)
Stream<T> sorted()
Java終端操作の例:
long count()
void forEach(Consumer<? super T> action)
Optional<T> findFirst()
<R> R collect(Collector<? super T, A, R> collector)
JavaStream を返すかどうかを見るだけで、
そのメソッドが中間操作か終端操作かが一瞬で分かります。
よくあるつまずきポイントと対処法
「中間操作だけ書いても何も起きない」
これは Stream の“遅延評価”が原因です。
names.stream().filter(s -> s.length() >= 4); // 何も起きない
Java必ず終端操作を付けてください。
names.stream()
.filter(s -> s.length() >= 4)
.forEach(System.out::println);
Java「途中で値を見たい」
peek を使います。
names.stream()
.filter(s -> s.length() >= 4)
.peek(s -> System.out.println("after filter: " + s))
.map(String::toUpperCase)
.peek(s -> System.out.println("after map: " + s))
.forEach(System.out::println);
Java「途中でリストにしたい」
一度 collect で終端させてから、新しいパイプラインを始めます。
List<String> filtered =
names.stream()
.filter(s -> s.length() >= 4)
.toList();
filtered.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
Javaまとめ:中間操作と終端操作を自分の言葉で整理する
中間操作と終端操作を一文でまとめるなら、
中間操作は「流れを変えるステップ」、終端操作は「流れを終わらせて結果を取り出すステップ」
です。
特に押さえておきたいのは、
- 中間操作は Stream を返すのでつなげられる
- 終端操作は Stream を返さないのでそこで終わる
- 中間操作は遅延評価で、終端操作が呼ばれるまで実行されない
- パイプラインは「左から右へ、型と意味の変化を追う」と読みやすい
という点です。
