Java8 Stream APIの基本(6) - 終端操作の概要
今回から、3回に分けてJava8 Stream API の終端操作について解説する。
終端操作は Stream
の中の要素を実際に処理をする工程であり、fliter
等の中間操作は終端操作をトリガーにして初めて実行される。 中間操作を適用した要素に対して、終端操作で処理し、最終的に Stream
から別のデータへの変換を行う。
JavaのStream API に終端操作があるのは、List
等のコレクションとStream
が別物になっているためだと思われる。この点で、同様の機能を持つ他の言語(RubyやScalaなど)とは、大きく仕組みが異なっている。
終端操作にはStream
から他のデータへの変換というStream API特有の操作と、集計・条件判定・畳み込み演算といった他の言語にも見られる操作の2つの側面がある。
終端操作のメソッド
終端操作には以下のものがある。
- 副作用 -
forEach
,forEachOrdered
- 集計 -
count
,min
,max
- 集計(プリミティブストリーム) -
sum
,average
- 条件判定 -
anyMatch
,findFirst
,findAny
,allMatch
,anyMath
,noneMatch
- 汎用処理・畳み込み演算 -
collect
,reduce
,toArray
終端操作は端的にいうと、Stream
内の各要素に対して、for
ループを最後まで(あるいは条件に一致するまで)回す処理に相当する。
よって、無限ストリームを扱う場合は無限ループにならないように、limit
で件数を制限するか、 short-circuitな終端操作を用いる必要がある。
また、終端操作は一度しか実行できない(close
メソッドについては後述する)。
終端操作を実行したあとのStream
変数に対する中間操作・終端操作は無効であり、例外が発生する(一般的には、Stream
型の変数を持ち回ったり、引数や戻り値にしない方がよい)。
ただし、Stream
を生成したコレクション等のデータからの再生成や、終端操作実施後のデータからストリームを生成するのは問題ない。
以降、各メソッドについて解説する。
副作用
forEach
, forEachOrdered
は、いずれもConsumer<T>
型のラムダ式(戻り値が無い)を取り、コンソール・ログ・ファイル出力等、任意の副作用を実行する。
両者の違いは、並列実行時の順序性であり、並列実行時に前者は順不同、後者は元の順番どおりに処理が実行される。
また、前回も述べたが、forEach
は、Collection
, Map
にも実装されており、中間操作が不要ならStream
を生成せずに実行することもできる。
集計
count
は要素数を計算する。Stream
は要素数不定のデータ型であるため、要素数算出のために最後の要素までループが実行されることに注意する必要がある。
また、実行後のストリームの再利用はできないので、 List#size
のような感覚で使ってはいけない。存在判定等でカウントを用いるなら次の節の条件判定メソッドを用いた方が良い。
min
, max
はそれぞれ要素の最小、最大となる要素1つを抽出する。Stream
の場合、大小判定を行うためComparator
を引数に渡す必要があるが、プリミティブStream(IntStream
など)の場合は引数は不要である。
戻り値はOptional<T>
型である(T
はStream
の要素と同じ)。これは値が存在しないかもしれないというデータの表現に用いる。
従来であればnull
か値を返す所に、Optinal<T>
型の値が返ってくることになる。
Optinal#isPresent
メソッドは 結果が存在するかをboolean
で返し、Optional#get
メソッドで要素を取得する必要がある。
他にも色々な機能があるが、詳細は次回以降の記事で述べる。
min
, max
は要素を1つ返すが、Stream
の要素が何もない場合は返すものが無くなるため、Optional
が戻り値となっている。
sum
, avarage
はプリミティブStream用の、合計、平均値の算出メソッドである。通常のStream
で同様の処理を行う場合はcollect
メソッドを用いる(collect
については次回解説する)。
条件判定
いずれもshort-circuitな終端操作であり、条件を満たすと全ての要素を処理せずに処理を打ち切る(break
に該当)。
この性質を生かして、効率的な処理を行える。
例えば、Stream
の中身が存在するかを判定する場合、
stream.count() > 0
とするより、
stream.findAny().isPresent()
とする方が効率が良い。前述したとおり、前者はStream
の全ての要素が処理されるためである。
findFirst
, findAny
は要素を1つ抽出し、Optional
型で返す。 前者は先頭の要素、後者は任意の要素を抽出する。
前回述べたが、findFirst
は順序が関係するので並列実行時は注意する。 効率低下や無限ストリームでの無限ループが起きる可能性がある。
allMatch
, anyMatch
, noneMatch
は、Predicate<T>
のラムダ式を取るメソッドであり、戻り値はboolean
である。
allMatch
はAnd条件であり、全ての要素がPredicate
を満たすか判定し、満たさない要素が発生した時点で処理を打ち切る。
anyMatch
はOr条件であり、全ての要素のうち、Predicate
を満たす要素が出てきた時点で処理を打ち切る。
noneMatch
は、全ての要素がPredicate
を満たさないことを判定し、 Predicate
がtrueを返した時点で処理を打ち切る。
いずれの要素も処理を打ち切るのが条件に合致する時のみであることに注意する。無限ストリームを用いて、条件を満たさない場合は無限ループとなる。
Stream.iterate(1, n -> (n + 1) % 10).anyMatch(n -> n >= 10);
汎用処理・畳み込み演算
要素の特定のデータ型の変換や前述のメソッドではできない特殊な集計などを行う場合に、collect
, reduce
を用いる。
( toArray
は配列変換のメソッドであり、特記事項は無いため割愛する。)
collect
は機能が豊富なため、詳細は次回述べる。
reduce
はcollect
の簡易版であり主に集計用途で用いる。
Java以外の言語では、リストの集計(リストの要素を1つにまとめるので畳み込み演算と呼ばれる)を行う機能は、fold
, reduce
等の名前で提供されており、
Stream#reduce
もそれに倣ったものと思われる。
但し、他の言語の同様の機能に比べると使い勝手が悪く、collect
を用いた方が良いことが多い。
reduce
は要素の中の2つの要素を1つにまとめるラムダ式を引数とし、全ての要素に連鎖的にラムダ式を適用することで1つの結果を作成する。
// 後ろからの文字列連結("CD" + "AB" が行われた後、 "EF" + "CDAB" が行われる。) String s = Stream.of("AB", "CD", "EF").reduce((a, b) -> b + a).get(); System.out.println(s); // EFCDAB
形式は他に初期値を取るものと、型変換も同時に行えるものの2つがある。
後者を使用する場合は、並列処理を踏まえた引数(combiner
-次回解説予定)を設定する必要があり、
collect
を使った方が簡潔になる可能性が高い。
reduce
は、型変換を行わずに集計できる場合に限られるため、通常はcollect
を用いるほうが良い。
次回は汎用的な終端操作であるcollect
について解説する。
[前多 賢太郎]