Streamのcollectメソッドを学ぶ
Stream にある数多くのメソッドの中でも分かり辛い感じがする collectメソッド について学びます。
collect メソッドの概要
端的に述べると collectメソッド は Stream<T> を R に変換する操作です。
より詳しく述べると、 Stream の各要素( T )を中間コンテナ( A )に折り畳んだ後に最終的な結果( R )に変換する操作です。
括弧内のアルファベットは Collector が持つ3つの型変数に対応しています。
- T : Streamの要素の型
- A : ミュータブル な中間コンテナの型
- R : 最終的に返される結果の型
例えば Stream<Character> を単純に繋げて String にする場合は、 Stream の各 Character ( T )を StringBuilder ( A )に append した後に String ( R )に変換する、 という流れになります。
Note
高パフォーマンスを得るため中間コンテナは ミュータブル となっています。 詳細は java.util.streamパッケージの「可変リダクション」 を参照ください。
Collector インターフェースの説明
collectメソッド は引数に Collector を取ります。 Collector は「関数を返す4つのメソッド」と「特性を返すメソッド」を持ったインターフェースです。
「特性」については後述するとして、まず「4つの関数」を説明します。
- supplier : 中間コンテナを生成する関数。 順次処理のとき最初の1回だけ実行される。 並列処理のときは複数回実行されることがある。
- accumulator : 中間コンテナへ値を折り畳む関数。 Stream の要素の数だけ実行される。
- combiner : ふたつの中間コンテナをひとつにマージする関数。 並列処理のときに実行されることがある。
- finisher : 中間コンテナから最終的な結果へ変換する。 最後の1回だけ実行される。
Note
日本語Javadocの説明文ではそれぞれ「サプライヤ」「アキュムレータ」「コンバイナ」「フィニッシャ」と表記されています。 勉強会などで読み方を牽制し合わなくて済みますね!
文字を結合する Collector の例
例えば Character の Stream を StringBuilder へ折り畳んで最終的に String に変換するという処理を考えてみます。
Collector が返す関数はそれぞれ次のような処理を行うようにします。
- supplier で StringBuilder のインスタンスを生成する
- accumulator で StringBuilder へ Character を append する
- combiner でふたつの StringBuilder をひとつにマージする
- finisher で StringBuilder を toString する
各関数のコードを記載します。
supplier
引数なしで StringBuilder のインスタンスを返します。
() -> new StringBuilder()
またはコンストラクタ参照でも良いです。
StringBuilder::new
accumulator
StringBuilder と Character を受け取って append します。 戻り値は void です。
(sb, c) -> sb.append(c)
またはメソッド参照でも良いです。
StringBuilder::append
combiner
ふたつの StringBuilder を受け取ってひとつの StringBuilder にマージして返します。
(sb1, sb2) -> sb1.append(sb2);
またはメソッド参照でも良いです。
StringBuilder::append
finisher
StringBuilder を受け取って String へ変換して返します。
sb -> sb.toString()
またはメソッド参照でも良いです。
StringBuilder::toString
これら4つの関数をもとにして Collector インスタンスを生成します。 愚直に Collector インターフェースを実装したクラスを作っても良いのですが Collector の ofメソッド を利用するのが楽です。
Collector<Character, StringBuilder, String> characterJoiner =
Collector.of(() -> new StringBuilder(), //supplier
(sb, c) -> sb.append(c), //accumulator
(sb1, sb2) -> sb1.append(sb2), //combiner
sb -> sb.toString())); //finisher
//コンストラクタ参照・メソッド参照バージョン
Collector<Character, StringBuilder, String> characterJoiner =
Collector.of(StringBuilder::new, //supplier
StringBuilder::append, //accumulator
StringBuilder::append, //combiner
StringBuilder::toString)); //finisher
この Collector を使って文字を連結してみます。
String s = Stream.of('h', 'e', 'l', 'l', 'o').collect(characterJoiner);
System.out.println(s); //hello
Collector の特性
Collector はネストした列挙型 Characteristics を使用してみっつの特性を表すことができます。 各特性について説明します。
CONCURRENT : ひとつの結果コンテナインスタンスに対して複数スレッドから accumulator を実行できる特性です。
つまり次のような処理を行っても不整合が起こらなければ、この特性を持っていると言えます。
A acc = supplier.get(); //中間コンテナ new Thread(() -> accumulator.accept(acc, t1)).start(); new Thread(() -> accumulator.accept(acc, t2)).start();
IDENTITY_FINISH : finisher が恒等関数であり、省略できる特性です。
つまり finisher が次のような実装になる場合、この特性を持っていると言えます。
Function<A, R> finisher = a -> (R) a;
UNORDERED : 操作が要素の順序に依存しない特性です。
いずれの特性も性能向上のためのものと思われます。 ですので特性をひとつも持たないとしても致命的な問題は無さそうです。 むしろ自作 Collector がどの特性を持っているか分からない、いまいち自信が無いなどの場合は Characteristics を設定しない方が良いかも知れませんね。
Collector インスタンスを生成する際に特性を与えたい場合は of メソッドの第5引数(可変長引数です)を使用します。
Collector<T, A, R> collector =
Collector.of(supplier, accumulator, combiner, finisher,
Characteristics.CONCURRENT,
Characteristics.IDENTITY_FINISH,
Characteristics.UNORDERED);
中間コンテナの型変数について
Collector は自分で実装しても良いですが、よく使われそうな実装を返す static メソッドを多数定義した Collectors というユーティリティクラスが提供されています。
Collectors のメソッド一覧を眺めて戻り値に注目するとほとんどが Collector<T, ?, R> となっており、 中間コンテナの型がワイルドカードで宣言されていることが分かります。
冒頭でも書きましたが Stream の collectメソッド は Stream<T> を R に変換する操作です。 このときの T と R は Collector<T, A, R> のそれに対応します。 つまり collectメソッド を使うひと―― Collector の利用者――にとっては中間コンテナが何であるか意識する必要はないんですね。
このように利用者には不要な中間コンテナの型が見えており、 実際にはワイルドカードが宣言されているというのは少し残念であり、 collectメソッド をややこしく感じさせている一因かも知れないな、と思います。
というわけで Collectors の各メソッドでのワイルドカードは空気のように扱うことにしましょう。
まとめ、それと自分への宿題
- 使う側としては中間コンテナの存在は無視る
- よく分からんかったら Characteristics は付与しない
- 何はともあれ collectメソッド 便利
こっから宿題。
Scalaの scan みたいなやつを実装してみる。
こんなやつです。
//これはScalaコード val xs = 1 to 5 toList xs.scan(0)(_ + _) //0, 1, 3, 6, 10, 15
追記:宿題やった
Streamのメソッドを操作の種類別で一覧にした
Stream の操作は 中間操作 と 終端操作 がありますが、 各メソッドがどちらの操作にあたるのか一覧にしてみました。
Note
中間操作と終端操作の詳細は ストリーム操作とパイプライン を参照してください。
ちなみに一覧中の T は Stream が取る型変数です。 例えば String[] をストリーム化した場合 T は String になります。
それから S は BaseStream が取る型変数で Stream<T> を指します。
中間操作
- Stream<T> filter(Predicate<? super T> predicate)
- <R> Stream<R> map(Function<? super T, ? extends R> mapper)
- IntStream mapToInt(ToIntFunction<? super T> mapper)
- LongStream mapToLong(ToLongFunction<? super T> mapper)
- DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper)
- <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
- IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper)
- LongStream flatMapToLong(Function<? super T, ? extends LongStream> mapper)
- DoubleStream flatMapToDouble(Function<? super T, ? extends DoubleStream> mapper)
- Stream<T> peek(Consumer<? super T> action)
- S sequential()
- S parallel()
- S unordered()
- S onClose(Runnable closeHandler)
ステートフルな中間操作
ステートフルな短絡中間操作
終端操作
- void forEach(Consumer<? super T> action)
- void forEachOrdered(Consumer<? super T> action)
- Object [] toArray()
- <A> A[] toArray(IntFunction<A[]> generator)
- T reduce(T identity, BinaryOperator<T> accumulator)
- Optional<T> reduce(BinaryOperator<T> accumulator)
- <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
- <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)
- <R, A> R collect(Collector<? super T, A, R> collector)
- Optional<T> min(Comparator<? super T> comparator)
- Optional<T> max(Comparator<? super T> comparator)
- long count()
- Iterator<T> iterator()
- Spliterator<T> spliterator()