Java | Java 詳細・モダン文法:Stream API 深掘り – Stream パイプライン

Java Java
スポンサーリンク

Stream パイプラインを一言でいうと

Stream パイプラインは、

「データの流れを、生成 → 中間操作 → 終端操作 という“流れ(パイプライン)”としてつなげて書くスタイル」

です。

for 文でゴリゴリ書いていた処理を、

「絞り込む」「変換する」「集計する」といった“ステップ”に分解して、
stream().filter(...).map(...).collect(...) のように、
“流れ”として表現するのが Stream パイプラインです。

ここをちゃんと理解すると、

「何をしているコードなのか」が一瞬で読めるようになるし、
自分で書くときも「処理の意図」をそのままコードに落とし込めるようになります。


Stream パイプラインの 3 要素

1. ストリームの生成(ソース)

最初に「どこからデータを流してくるか」を決めます。

典型的にはコレクションからです。

List<String> names = List.of("Alice", "Bob", "Charlie");

Stream<String> stream = names.stream();
Java

names.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

このパイプラインを「日本語の文章」にするとこうです。

  1. names.stream()
    → 「名前のリストからストリームを作る」
  2. .filter(name -> name.length() >= 4)
    → 「4 文字以上の名前だけを残す」
  3. .map(name -> name.length())
    → 「名前を、その長さ(整数)に変換する」
  4. .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

この時点では、filtermap も「こういう処理をしますよ」という“宣言”を積み重ねただけです。

終端操作を付けると、初めて実行されます。

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

このパイプラインを「処理のステップ」として整理すると:

  1. ソース:users.stream()
    → 「ユーザーの流れ」
  2. 中間:filter(u -> u.age >= 20)
    → 「20 歳以上だけに絞る」
  3. 中間:map(u -> u.name)
    → 「User → String(名前)に変換」
  4. 中間:sorted()
    → 「名前でソート」
  5. 終端: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);
Java

peek は「中間操作」で、
「要素をそのまま流しつつ、横で何かする(主にログ)」ためのメソッドです。

ただし、peek も他の中間操作と同じく「終端操作が呼ばれるまで実行されない」ので、
forEachcollect などの終端操作とセットで使う必要があります。

「途中でリストにしたい」「一回区切りたい」

パイプラインは基本的に「一筆書き」で書くのがきれいですが、
途中で一度 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 を返さない」ので、そこでパイプラインが終わる
中間操作は“遅延評価”で、終端操作が呼ばれるまで実行されない
パイプラインは「左から右へ、型と意味の変化を追う」と読みやすい

という点です。

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