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 )に変換する、 という流れになります。
Collector インターフェースの説明
collectメソッドは引数にCollectorを取ります。
Collectorは「関数を返す4つのメソッド」と「特性を返すメソッド」を持ったインターフェースです。
「特性」については後述するとして、まず「4つの関数」を説明します。
- supplier: 中間コンテナを生成する関数。順次処理のとき最初の1回だけ実行される。並列処理のときは複数回実行されることがある。
-
accumulator: 中間コンテナへ値を折り畳む関数。
Streamの要素の数だけ実行される。 - combiner: ふたつの中間コンテナをひとつにマージする関数。並列処理のときに実行されることがある。
- finisher: 中間コンテナから最終的な結果へ変換する。 最後の1回だけ実行される。
文字を結合する 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
Discussion