「Streamって何をしているの?」

例えば、 数字のListがあるとします。

List<Integer> numbers =
        Arrays.asList(1, 2, 3, 4, 5);

このListから、 「3より大きい数字だけ取り出したい」 とします。

普通に書けば、 for を使って処理できます。

for (Integer number : numbers) {

    if (number > 3) {

        System.out.println(number);

    }

}

これでも問題ありません。

ただ、Javaでは、 Listなどのデータを 「条件で絞る」 「別の値に変換する」 「最後にまとめる」 といった処理を、 Stream APIを使って書くことができます。

Streamは、データを順番に加工していくための仕組み

まずはListからStreamを作る

Streamを使うには、 Listなどのコレクションから Streamを作ります。

List<Integer> numbers =
        Arrays.asList(1, 2, 3, 4, 5);


numbers.stream();

この stream() によって、 Listのデータを Streamとして処理できるようになります。

イメージ

List
1 / 2 / 3 / 4 / 5

↓ stream()

Stream
「このデータを順番に処理していく」

ここでは、 Streamそのものを 特別なデータ構造として覚える必要はありません。

まずは、

「Listのデータを加工するためにStreamにする」

と考えておけば十分です。

filter。条件に合うデータだけ残す

Streamでよく使うのが、 filter() です。

例えば、 3より大きい数字だけ残したいとします。

List<Integer> result =
        numbers.stream()
               .filter(n -> n > 3)
               .collect(Collectors.toList());

元のデータが、

1
2
3
4
5

だった場合、 filter() によって、

4
5

だけが残ります。

filter 条件に合うデータを残す
n -> n > 3 「3より大きい」という条件

つまり、 filter() を見たら、

「何を残しているんだろう?」

と考えると読みやすくなります。

map。データを別の形に変換する

次に、 map() です。

map() は、 データを別の値に変換するときに使います。

例えば、 数字をすべて2倍にします。

List<Integer> result =
        numbers.stream()
               .map(n -> n * 2)
               .collect(Collectors.toList());

元のデータは、

1
2
3
4
5

ですが、 map() によって、

2
4
6
8
10

に変換されます。

filter データを絞る
map データを変換する

例えば業務コードでは、 UserオブジェクトのListから ユーザー名だけを取り出す、 といった使い方があります。

List<String> names =
        users.stream()
             .map(User::getName)
             .collect(Collectors.toList());

ここでは、 User そのものを扱うのではなく、 name に変換しています。

filterとmapは組み合わせられる

Streamでは、 複数の処理をつなげることができます。

例えば、 「3より大きい数字だけを取り出して、 さらに2倍にする」 という処理です。

List<Integer> result =
        numbers.stream()
               .filter(n -> n > 3)
               .map(n -> n * 2)
               .collect(Collectors.toList());

処理の流れを見ると、

処理の流れ

1 / 2 / 3 / 4 / 5

↓ filter
4 / 5

↓ map
8 / 10

↓ collect
Listとして取得

Streamは「処理をつなげて書ける」

ここが、 Stream APIを使う大きな特徴の一つです。

collect。処理した結果をまとめる

Streamの処理結果を、 Listなどとして取得するときに よく使うのが collect() です。

List<Integer> result =
        numbers.stream()
               .filter(n -> n > 3)
               .collect(Collectors.toList());

ここで、 filter() はデータを絞っています。

そして、 collect() で、 その結果をListとして取得しています。

filter データを絞る
map データを変換する
collect 結果をまとめる

Streamを読むときは、 この3つをまず見分けられるようになると、 かなり楽になります。

reduce。複数のデータを1つにまとめる

Streamには、 reduce() という処理もあります。

これは、 複数のデータを 最終的に1つの値へまとめるときに使います。

例えば、 数字をすべて足してみます。

List<Integer> numbers =
        Arrays.asList(1, 2, 3, 4, 5);


int total =
        numbers.stream()
               .reduce(0, Integer::sum);

処理のイメージは、

reduceのイメージ

1 / 2 / 3 / 4 / 5


1 + 2 + 3 + 4 + 5


15

つまり、

reduce → 複数のデータを1つの結果にまとめる

と考えると分かりやすいです。

例えば、 合計値、最大値、最小値など、 複数のデータから 一つの結果を求める場面で使えます。

ここでは深追いしない

reduceは便利ですが、 初めから細かい動作まで 覚える必要はありません。

まずは、 「複数のデータを1つにまとめる」 という役割を理解しておけば十分です。

mapとreduceを続けて使う

map()reduce() は、 一緒に使うこともできます。

List<Integer> numbers =
        Arrays.asList(1, 2, 3, 4, 5);


int total =
        numbers.stream()
               .map(n -> n * 2)
               .reduce(0, Integer::sum);

この場合、

処理の流れ

1 / 2 / 3 / 4 / 5

↓ map
2 / 4 / 6 / 8 / 10

↓ reduce
30

つまり、

map それぞれのデータを変換する
reduce 最後に1つの結果へまとめる

「map/reduce」という一つの処理ではなく、 mapとreduceという別々の操作を組み合わせている と考えておくと分かりやすいです。

中間操作と終端操作

Streamを理解するとき、 もう一つ知っておきたいのが、 「中間操作」と「終端操作」です。

例えば、

numbers.stream()
        .filter(n -> n > 3)
        .map(n -> n * 2)
        .collect(Collectors.toList());

この中では、

filter 中間操作
map 中間操作
collect 終端操作

中間操作は、 データを加工しながら、 次の処理へつないでいきます。

一方、 終端操作では、 Streamの処理を終えて、 最終的な結果を取得します。

ざっくり覚えるなら

filter / map
→ データを加工する

collect / reduce
→ Streamを終えて結果を取得する

まずはこの程度の理解で十分です。

Streamを見たら、処理の流れを見る

実際の業務コードでは、 Streamが一行で終わらないこともあります。

List<String> names =
        users.stream()
             .filter(user -> user.isActive())
             .map(User::getName)
             .filter(name -> name != null)
             .collect(Collectors.toList());

初めて見ると、 少し長く感じます。

しかし、 一つずつ分解すると、

処理の流れ

users

↓ filter
有効なユーザーだけ

↓ map
User → name

↓ filter
nullではないnameだけ

↓ collect
List<String>

Streamは上から順番に読めばいい

ということが分かります。

業務コードを読むときも、 一気に理解しようとせず、 「この処理で何が変わった?」 と一つずつ追っていくと読みやすくなります。

Streamを見たら、まず処理を分解する

業務コードで、 いきなり長いStreamが出てきても、 慌てる必要はありません。

まず、 stream() の後ろを、 一つずつ追っていきます。

list.stream()
    .filter(...)
    .map(...)
    .collect(...);

この場合は、

読み方

filter
→ 何を残している?

map
→ 何に変換している?

collect
→ 最終的に何へまとめている?

これだけでも、 Streamの処理内容はかなり追えるようになります。

そして、 reduce() が出てきたら、

「複数のデータを、最終的に一つへまとめている」

と考えてみる。

Streamは、 書き方を暗記するより、 「データがどう変化しているか」 を追う方が理解しやすいです。

Streamを使えば必ず良いわけではない

Streamは便利ですが、 すべての処理をStreamで書けばいいわけではありません。

複雑な分岐や、 途中で処理を抜ける必要がある場合など、 for 文の方が読みやすいこともあります。

大切なのは、 「Streamを使うこと」ではなく、 「処理の流れを分かりやすく書くこと」 です。

今回のポイント

Stream APIは、 Listなどのデータを 順番に加工していくための仕組みです。

まずは、 それぞれの役割を覚えておけば十分です。

覚えておきたいStreamの基本

stream()
→ データをStreamとして処理する

filter()
→ 条件に合うデータを残す

map()
→ データを別の形へ変換する

collect()
→ 結果をListなどにまとめる

reduce()
→ 複数のデータを1つの結果へまとめる

そして、 Streamのコードを読むときは、

「この処理でデータがどう変化しているのか?」

という視点を持つことが大切です。

filtermapcollectreduce をそれぞれ単体で覚えるよりも、 一連のデータの流れとして見ると、 Streamはかなり分かりやすくなります。

最初から複雑なStreamを書ける必要はありません。

まずは、

「絞る → 変換する → まとめる」

という基本的な流れを理解しておけば、 実際の業務コードを読むときにも役立ちます。