⛄️

Streamのcollectメソッドを学ぶ

2014/10/04に公開

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 の例

例えば CharacterStreamStringBuilder へ折り畳んで最終的に String に変換するという処理を考えてみます。

Collector が返す関数はそれぞれ次のような処理を行うようにします。

  • supplierStringBuilder のインスタンスを生成する
  • accumulatorStringBuilderCharacterappend する
  • combinerでふたつの StringBuilder をひとつにマージする
  • finisherStringBuildertoString する

各関数のコードを記載します。

  • supplier
    引数なしで StringBuilder のインスタンスを返します。

    () -> new StringBuilder()
    

    またはコンストラクタ参照でも良いです。

    StringBuilder::new
    
  • accumulator
    StringBuilderCharacter を受け取って 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 インターフェースを実装したクラスを作っても良いのですが Collectorofメソッドを利用するのが楽です。

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> となっており、中間コンテナの型がワイルドカードで宣言されていることが分かります。

冒頭でも書きましたが StreamcollectメソッドStream<T>R に変換する操作です。 このときの TRCollector<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