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