INTRODUCTION
「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は、データを順番に加工していくための仕組み
01
まずは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にする」
と考えておけば十分です。
02
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()
を見たら、
「何を残しているんだろう?」
と考えると読みやすくなります。
03
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
に変換されます。
例えば業務コードでは、 UserオブジェクトのListから ユーザー名だけを取り出す、 といった使い方があります。
List<String> names =
users.stream()
.map(User::getName)
.collect(Collectors.toList());
ここでは、
User
そのものを扱うのではなく、
name
に変換しています。
04
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を使う大きな特徴の一つです。
05
collect。処理した結果をまとめる
Streamの処理結果を、
Listなどとして取得するときに
よく使うのが
collect()
です。
List<Integer> result =
numbers.stream()
.filter(n -> n > 3)
.collect(Collectors.toList());
ここで、
filter()
はデータを絞っています。
そして、
collect()
で、
その結果をListとして取得しています。
Streamを読むときは、 この3つをまず見分けられるようになると、 かなり楽になります。
06
reduce。複数のデータを1つにまとめる
Streamには、
reduce()
という処理もあります。
これは、 複数のデータを 最終的に1つの値へまとめるときに使います。
例えば、 数字をすべて足してみます。
List<Integer> numbers =
Arrays.asList(1, 2, 3, 4, 5);
int total =
numbers.stream()
.reduce(0, Integer::sum);
処理のイメージは、
1 / 2 / 3 / 4 / 5
↓
1 + 2 + 3 + 4 + 5
↓
15
つまり、
reduce → 複数のデータを1つの結果にまとめる
と考えると分かりやすいです。
例えば、 合計値、最大値、最小値など、 複数のデータから 一つの結果を求める場面で使えます。
reduceは便利ですが、
初めから細かい動作まで
覚える必要はありません。
まずは、
「複数のデータを1つにまとめる」
という役割を理解しておけば十分です。
07
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」という一つの処理ではなく、 mapとreduceという別々の操作を組み合わせている と考えておくと分かりやすいです。
08
中間操作と終端操作
Streamを理解するとき、 もう一つ知っておきたいのが、 「中間操作」と「終端操作」です。
例えば、
numbers.stream()
.filter(n -> n > 3)
.map(n -> n * 2)
.collect(Collectors.toList());
この中では、
中間操作は、 データを加工しながら、 次の処理へつないでいきます。
一方、 終端操作では、 Streamの処理を終えて、 最終的な結果を取得します。
filter / map
→ データを加工する
collect / reduce
→ Streamを終えて結果を取得する
まずはこの程度の理解で十分です。
09
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は上から順番に読めばいい
ということが分かります。
業務コードを読むときも、 一気に理解しようとせず、 「この処理で何が変わった?」 と一つずつ追っていくと読みやすくなります。
FIELD NOTE
Streamを見たら、まず処理を分解する
業務コードで、 いきなり長いStreamが出てきても、 慌てる必要はありません。
まず、
stream()
の後ろを、
一つずつ追っていきます。
list.stream()
.filter(...)
.map(...)
.collect(...);
この場合は、
filter
→ 何を残している?
map
→ 何に変換している?
collect
→ 最終的に何へまとめている?
これだけでも、 Streamの処理内容はかなり追えるようになります。
そして、
reduce()
が出てきたら、
「複数のデータを、最終的に一つへまとめている」
と考えてみる。
Streamは、 書き方を暗記するより、 「データがどう変化しているか」 を追う方が理解しやすいです。
Streamは便利ですが、
すべての処理をStreamで書けばいいわけではありません。
複雑な分岐や、
途中で処理を抜ける必要がある場合など、
for
文の方が読みやすいこともあります。
大切なのは、
「Streamを使うこと」ではなく、
「処理の流れを分かりやすく書くこと」
です。
SUMMARY
今回のポイント
Stream APIは、 Listなどのデータを 順番に加工していくための仕組みです。
まずは、 それぞれの役割を覚えておけば十分です。
stream()
→ データをStreamとして処理する
filter()
→ 条件に合うデータを残す
map()
→ データを別の形へ変換する
collect()
→ 結果をListなどにまとめる
reduce()
→ 複数のデータを1つの結果へまとめる
そして、 Streamのコードを読むときは、
「この処理でデータがどう変化しているのか?」
という視点を持つことが大切です。
filter、
map、
collect、
reduce
をそれぞれ単体で覚えるよりも、
一連のデータの流れとして見ると、
Streamはかなり分かりやすくなります。
最初から複雑なStreamを書ける必要はありません。
まずは、
「絞る → 変換する → まとめる」
という基本的な流れを理解しておけば、 実際の業務コードを読むときにも役立ちます。