StreamAPIの細かい挙動 ステートフルな中間操作と終端操作
始めに問題です。
以下のコードのコンソール出力はどうなるでしょうか。
//問1
Stream.of(1, 2, 3, 4, 5).map(i -> {
System.out.println(i);
return i * 2;
}).filter(i -> i > 3).findFirst();
//問2
Stream.of(1, 2, 3, 4, 5).peek(System.out::println).map(i -> {
return i * 2;
}).filter(i -> i > 3).findFirst();
//問3
Stream.of(1, 2, 3, 4, 5).peek(System.out::println).map(i -> {
return i * 2;
}).filter(i -> i > 3).sorted().findFirst();
//問4
Stream.of(1, 2, 3, 4, 5).peek(System.out::println).map(i -> {
return i * 2;
}).filter(i -> i > 3).distinct().findFirst();
答え
//問1
1
2
//問2
1
2
//問3
1
2
3
4
5
//問4
1
2
解説
問1
javaのStreamの処理は、「生成」「中間操作」「終端操作」に分けられます。
基本的に、生成がStreamを作成している部分、終端操作が最終的に結果を返すメソッド、
中間操作がそれ以外のStreamからStreamを返す部分と考えればOKです。
実際の処理は終端操作が実行されたタイミングで実行されます。
そして基本的にStreamに流れる各要素に対し終端操作まで実行します。
すべての要素にmapをかけてから、filterをかけるわけではありません。
(例えば、javascriptの配列で似たようなコードを書いた場合と挙動が異なります。)
そして、今回終端操作として採用しているFindFirstは終端操作の中でも短絡終端操作と呼ばれるものです。
「短絡」のつく操作は、条件さえ満たせばStreamで流れてくるすべての要素が評価される前でも処理を終了します。
(短絡操作が条件を満たせば、無限に値を返すStream(hasNextが常にtrueを返すiteratorから作ったStreamなど)に対しても無限ループに陥らせずに処理を終えることが出来ます。)
forやwhileのループで、一つでも条件を満たしたらbreakして残りは無視する処理はよくありますが、それを自動でやってくれるわけです。中々賢い。
今回のケースでは、2が流れてきた時点でfindFirstが値を返すことが出来るので、1、2まででStream処理が中断されます。
問2
基本的には問1と同じです。
5つの要素が流れてくるStreamの生成の直後にpeekを置くと、5回実行されそうなものですが、
末尾が短絡終端処理のためStreamの処理は2までで止まります。
問3
短絡終端処理を行っていてもすべてのStreamに流れる要素が評価されるケースがあります。
「ステートフルな中間処理」を間に挟んだ場合です。
この場合だとsortedがステートフルな中間処理です。
ソートした上で最初に条件を満たすものを取得するという処理なので、
論理的にもすべての要素を参照する必要があるのは当然ですね。
ちなみに、各中間処理がステートフルなのかどうかは公式リファレンスに書いてあります。
問4
ちなみに、ステートフルな中間操作を挟むと常にすべての要素を参照する必要があるかというとそういうわけではありません。
distinctもステートフルな中間操作ですが、これも1,2で処理が完了します。
というより、実際通常の順序ありStreamで全要素が常に参照されるのはsortedを使ったときくらいです。
まとめ
- Streamは各要素ごとに終端操作まで処理される
- 短絡終端処理を使う場合、Streamがよろしく処理を最適化してくれる
- ステートフルな中間操作(sorted)の場合は例外があるので注意
余談
ParallelStreamの場合はちょっと挙動が異なりますが、
長くなるのでこの辺りで。