エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

Java8 Stream APIの基本(6) - 終端操作の概要

今回から、3回に分けてJava8 Stream API の終端操作について解説する。

終端操作は Stream の中の要素を実際に処理をする工程であり、fliter 等の中間操作は終端操作をトリガーにして初めて実行される。 中間操作を適用した要素に対して、終端操作で処理し、最終的に Stream から別のデータへの変換を行う。

JavaのStream API に終端操作があるのは、List等のコレクションとStreamが別物になっているためだと思われる。この点で、同様の機能を持つ他の言語(RubyScalaなど)とは、大きく仕組みが異なっている。
終端操作には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>型である(TStreamの要素と同じ)。これは値が存在しないかもしれないというデータの表現に用いる。 従来であれば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は機能が豊富なため、詳細は次回述べる。

reducecollectの簡易版であり主に集計用途で用いる。 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について解説する。

[前多 賢太郎]