ループの代わりにパイプラインを
Stream とは、値の並びを処理するためのパイプラインです。ソース(多くはコレクション)から始め、「これだけ残す」「一つずつ変換する」「並べ替える」といった操作をつなぎ、最後に結果を集めて締めくくります。一時的なリストと内側の if を伴うループを書く代わりに、何が欲しいかを記述し、走査は Stream に任せます。
ラムダを身につけた今なら、Stream はすっと腑に落ちます。どの操作も、要素一つに対する処理を記述したラムダ(またはメソッド参照)を受け取ります。
上から下へ読みます:名前を取って、長いものを残し、大文字にして、リストに集める。元の names リストは決して変更されません - Stream は新しい結果を生み出し、ソースはそのままにします。
Stream の構造
どのパイプラインにも 3 つの部分があります。
- ソース -
list.stream()、Arrays.stream(array)、またはStream.of(a, b, c)。 - 0 個以上の中間操作 -
filter、map、sorted、distinct、limit。それぞれが別の Stream を返すので、チェーンでつなげられます。 - ちょうど 1 つの終端操作 -
collect、count、forEach、reduce、findFirst。これが処理を起動し、結果(または副作用)を生み出します。
この区別が重要なのは、最初は誰もが驚く一つのルールがあるからです。
中間操作は遅延評価される
中間操作は、それ自体では何もしません。何が起こるべきかを記録するだけです。パイプラインは、終端操作が結果を要求して初めて実行されます。
実行してみてください:map のラムダは一度も実行されません。終端操作を加えると、チェーン全体が動き出します。
この遅延評価は戦うべき癖ではありません。Stream が操作を融合し、不要な処理を省くのを可能にします。ただし、終端操作のないパイプラインは何もしないことを意味し、これは「なぜ何も起きないの?」という、よくあるバグです。
filter と map:残すと変換する
この 2 つが大半の役割を担います。filter は Predicate(boolean を返すラムダ)を取り、通過した要素を残します。map は Function を取り、各要素をそれを適用した結果で置き換えます。
map は要素の型を変えられる点に注目してください。ここでは String の Stream が Integer の Stream になります。String::length はメソッド参照で、ラムダ w -> w.length() の短縮形です。
終端操作は結果を生み出す
Stream の形を整えたら、終端操作がそれを具体的な何かに変えます。
よく使う終端操作:collect(リスト/セット/マップに集める)、count、forEach、anyMatch / allMatch / noneMatch、findFirst、min / max、そして reduce。終端操作が実行されると、Stream は消費済みになり、再利用できません。新しいパイプラインが欲しければ list.stream() をもう一度呼び出します。
結果を集める
Collectors を伴う collect は、結果を組み立てる主力です。最もよく使うのは Collectors.toList() です。
Collectors には toSet()、joining(...)、groupingBy(...)、counting() もあります。Java 16 以降なら、collect(Collectors.toList()) をより短い .toList() に置き換えられます(変更不可のリストを返します)。
List<String> result = names.stream().map(String::toUpperCase).toList();
sorted、distinct、limit
これらの中間操作は、集める前に Stream を整え直します。
引数なしの sorted() は自然順序を使います。独自の順序にしたいときは Comparator(ここではラムダ)を渡します。降順に並べるなら Comparator.reverseOrder() のほうがすっきりします。
reduce:Stream を一つの値に畳み込む
すべての要素を一つの結果にまとめたいとき - 合計、積、最も長い文字列 - reduce が汎用ツールです。初期値と、2 つの値を結合する関数を渡します。
単純な合計や平均なら、専用の Stream のほうが明快です:nums.stream().mapToInt(Integer::intValue).sum()。出来合いの集計手段がないときに reduce を使いましょう。
Stream はすべてのループを置き換えるわけではない
Stream は、コレクションを結果へ変換するときに輝きます。ループより自動的に速いわけではなく、外部の状態を書き換えたり、込み入った形で途中で抜けたりする必要があるときは扱いにくくなります。よい目安:パイプラインが一つの明快な文として読めるなら Stream を使い、共有のカウンターや index に手を伸ばしているなら、素直な for ループのほうが正直で十分です。
また、Stream は一度きりしか使えないことも覚えておきましょう。これは例外を投げます。
Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count(); // IllegalStateException: stream has already been operated upon
次へ:Optional
いくつかの Stream の終端操作 - findFirst、min、max、初期値なしの reduce - は何も見つからないことがあるため、むき出しの値を返しません。代わりに Optional を返します。「値があるかもしれないし、何もないかもしれない」を表す Java のコンテナで、ようやく null を返すことへのきれいな代替手段を与えてくれます。それが次のページです。
よくある質問
Java の Stream とは何ですか?
Stream とは、要素の並び(多くはコレクションから取得したもの)を filter、map、sorted などの一連の操作を通して処理し、collect や count といった終端操作で締めくくるパイプラインです。データを保持せず、ソースも変更せず、計算を記述するだけです。list.stream() で生成し、チェーンはレシピのように上から下へ読みます。
Java で Stream を再びリストに変換するには?
パイプラインを終端コレクターで締めくくります:list.stream().filter(...).collect(Collectors.toList())。Java 16 以降なら、より短い .toList() を使えます(変更不可のリストを返します)。終端操作がなければ何も実行されません。filter や map などの中間操作は遅延評価だからです。
for ループではなく Stream を使うべきなのはどんなときですか?
コレクションを変換・絞り込んで結果を得るときは Stream を選びましょう。パイプラインは一つの明快な文として読めます(「名前を取って、長いものだけ残し、大文字にして、リストに集める」)。外部の状態を書き換える必要がある場合、複雑な形で途中で抜ける場合、あるいは手続き的なステップとして書いたほうが単純な場合は、素直な for ループを使い続けましょう。Stream は生の速さではなく、明快さのためのものです。