Stream パイプラインを一言でいうと
Stream パイプラインは、
「データの流れを、生成 → 中間操作 → 終端操作 という“流れ(パイプライン)”としてつなげて書くスタイル」
です。
for 文でゴリゴリ書いていた処理を、
「絞り込む」「変換する」「集計する」といった“ステップ”に分解して、stream().filter(...).map(...).collect(...) のように、
“流れ”として表現するのが Stream パイプラインです。
ここをちゃんと理解すると、
「何をしているコードなのか」が一瞬で読めるようになるし、
自分で書くときも「処理の意図」をそのままコードに落とし込めるようになります。
Stream パイプラインの 3 要素
1. ストリームの生成(ソース)
最初に「どこからデータを流してくるか」を決めます。
典型的にはコレクションからです。
List<String> names = List.of("Alice", "Bob", "Charlie");
Stream<String> stream = names.stream();
Javanames.stream() が「ソース」です。
ここから「名前の流れ」が始まります。
他にも、配列、Stream.of(...)、Files.lines(...)、Stream.generate(...) など、
いろいろなソースがありますが、最初は「List から stream()」だけで十分です。
2. 中間操作(intermediate operations)
中間操作は、「ストリームを別のストリームに変換する操作」です。
代表的なものは:
filter(絞り込み)map(変換)sorted(ソート)distinct(重複排除)limit / skip(一部だけ取る/飛ばす)
などです。
中間操作の特徴は、
「Stream を返す」
ことです。
Stream<String> s1 = names.stream();
Stream<String> s2 = s1.filter(name -> name.length() >= 4);
Stream<Integer> s3 = s2.map(name -> name.length());
Javaこのように、「ストリームを受け取って、ストリームを返す」ので、
何個でもつなげていけます。
3. 終端操作(terminal operation)
終端操作は、「ストリームの流れを“終わらせて”、結果を取り出す操作」です。
代表的なものは:
collect(リストなどに集める)forEach(1 件ずつ処理する)count(件数を数える)findFirst / findAny(1 件だけ取り出す)anyMatch / allMatch / noneMatch(条件判定)
などです。
終端操作の特徴は、
「Stream を返さない」
ことです。
long count = names.stream()
.filter(name -> name.length() >= 4)
.count(); // ここでパイプラインが終わる
Java終端操作を呼んだ瞬間に、
それまでに積み重ねてきた中間操作が「まとめて実行される」イメージです。
具体例で「パイプラインの流れ」を目で追う
例1:名前リストから「長さ 4 以上の名前の長さリスト」を作る
まずは、よくあるパターンから。
import java.util.List;
import java.util.stream.Collectors;
public class StreamPipelineExample1 {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie", "Ann");
List<Integer> lengths =
names.stream() // ソース:List → Stream<String>
.filter(name -> name.length() >= 4) // 中間:4 文字以上に絞る
.map(name -> name.length()) // 中間:String → Integer に変換
.collect(Collectors.toList()); // 終端:List<Integer> に集める
System.out.println(lengths); // [5, 7, 3? ではなく [5, 7, 3] ではない → 実際は [5, 7, 3]? → Ann は 3 なので除外される → [5, 7]
}
}
Javaこのパイプラインを「日本語の文章」にするとこうです。
names.stream()
→ 「名前のリストからストリームを作る」.filter(name -> name.length() >= 4)
→ 「4 文字以上の名前だけを残す」.map(name -> name.length())
→ 「名前を、その長さ(整数)に変換する」.collect(Collectors.toList())
→ 「結果をリストに集める」
この「1 行ごとに“何をしているか”が明確」なのが、パイプラインの一番の良さです。
パイプラインは「宣言」であり、「実行」は終端操作のときだけ
遅延評価(lazy evaluation)のイメージ
Stream の中間操作は、「その場では実行されません」。
例えば、次のコードを見てください。
List<String> names = List.of("Alice", "Bob", "Charlie");
names.stream()
.filter(name -> {
System.out.println("filter: " + name);
return name.length() >= 4;
})
.map(name -> {
System.out.println("map: " + name);
return name.length();
});
// ここまででは、まだ何も出力されない
Javaこの時点では、filter も map も「こういう処理をしますよ」という“宣言”を積み重ねただけです。
終端操作を付けると、初めて実行されます。
long count =
names.stream()
.filter(name -> {
System.out.println("filter: " + name);
return name.length() >= 4;
})
.map(name -> {
System.out.println("map: " + name);
return name.length();
})
.count(); // ここで初めて実行される
Java実行順序は、
filter(Alice) → map(Alice)filter(Bob) → (false なので map は呼ばれない)filter(Charlie) → map(Charlie)
というように、「1 要素ずつ、パイプラインを通して処理される」形になります。
ここで大事なのは、
「中間操作は“遅延評価”で、終端操作が呼ばれるまで実行されない」
ということです。
パイプラインの設計感覚:処理を「ステップ」に分解する
例2:ユーザーリストから「20 歳以上の名前だけをソートして表示」
オブジェクトを扱う例で、パイプラインの設計を見てみましょう。
import java.util.List;
class User {
final String name;
final int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
}
public class StreamPipelineExample2 {
public static void main(String[] args) {
List<User> users = List.of(
new User("Alice", 20),
new User("Bob", 17),
new User("Charlie", 25),
new User("Ann", 30)
);
users.stream()
.filter(u -> u.age >= 20) // 20 歳以上に絞る
.map(u -> u.name) // 名前だけ取り出す
.sorted() // 名前でソート
.forEach(System.out::println); // 1 行ずつ表示
}
}
Javaこのパイプラインを「処理のステップ」として整理すると:
- ソース:
users.stream()
→ 「ユーザーの流れ」 - 中間:
filter(u -> u.age >= 20)
→ 「20 歳以上だけに絞る」 - 中間:
map(u -> u.name)
→ 「User → String(名前)に変換」 - 中間:
sorted()
→ 「名前でソート」 - 終端:
forEach(System.out::println)
→ 「1 行ずつ表示」
このように、「何をしたいか」をそのままコードに落とし込めるのがパイプラインの強みです。
パイプラインの“読み方”を身につける
左から右へ、「データの変化」を追う
Stream パイプラインを読むときは、
「左から右へ、データの型と意味がどう変わっていくか」 を追ってください。
さっきの例で言うと:
users.stream()
→ 型:Stream<User>
→ 意味:「ユーザーの流れ」
.filter(u -> u.age >= 20)
→ 型:Stream<User>(変わらない)
→ 意味:「20 歳以上のユーザーの流れ」
.map(u -> u.name)
→ 型:Stream<String>
→ 意味:「20 歳以上のユーザーの“名前”の流れ」
.sorted()
→ 型:Stream<String>
→ 意味:「ソートされた名前の流れ」
.forEach(...)
→ 型:void(ここで終わり)
→ 意味:「各名前に対して何かする」
この「型の変化」と「意味の変化」をセットで追えるようになると、
パイプラインは“読むのが楽しいコード”になります。
よくあるつまずきポイントと、パイプライン的な考え方
「途中でデバッグしたい」「ログを出したい」
パイプラインの途中で「今どんな値が流れているか」を見たくなることがあります。
そのときに便利なのが peek です。
users.stream()
.filter(u -> u.age >= 20)
.peek(u -> System.out.println("after filter: " + u.name))
.map(u -> u.name)
.peek(name -> System.out.println("after map: " + name))
.sorted()
.forEach(System.out::println);
Javapeek は「中間操作」で、
「要素をそのまま流しつつ、横で何かする(主にログ)」ためのメソッドです。
ただし、peek も他の中間操作と同じく「終端操作が呼ばれるまで実行されない」ので、forEach や collect などの終端操作とセットで使う必要があります。
「途中でリストにしたい」「一回区切りたい」
パイプラインは基本的に「一筆書き」で書くのがきれいですが、
途中で一度 List にしてから別の処理をしたいこともあります。
その場合は、一度 collect で終端させてから、
新しいパイプラインを始める、という形になります。
List<String> filtered =
names.stream()
.filter(name -> name.length() >= 4)
.collect(Collectors.toList());
filtered.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
Java「どこまでを 1 本のパイプラインにするか」は設計のセンスですが、
「意味のまとまり」で区切ると読みやすくなります。
まとめ:Stream パイプラインを自分の言葉で整理する
Stream パイプラインをあなたの言葉でまとめるなら、
「データの処理を、ソース → 中間操作 → 終端操作 という“流れ”としてつなげて書くスタイル」
です。
特に意識しておきたいのは、
中間操作は「Stream を返す」ので、何個でもつなげられる
終端操作は「Stream を返さない」ので、そこでパイプラインが終わる
中間操作は“遅延評価”で、終端操作が呼ばれるまで実行されない
パイプラインは「左から右へ、型と意味の変化を追う」と読みやすい
という点です。

