倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

蒐集してやんよ java.util.stream.Collectors クラス (5) - 独自コレクタを作ってみよう! -

Java の Collectors クラスに定義されているメソッドを見ていくシリーズ(目次)。 前回 parallel ストリームと concurrent コレクタに関してあれこれ試したワリにはにいまいちしっくり来てない状態なのですが、今回は独自コレクタを作ってみます。

あまりキチンと言及してませんでしたが、Java 8 のコレクタには大まかに2つに分類することができます。 それは「値を返す」か「コンテナ・オブジェクトを返す」かです。 コンテナ・オブジェクトとはコレクションや配列のような、(同一の型の)オブジェクトを複数保持したオブジェクトのことを指しており、状態を持つという性質があります。 この意味で StringBuilder や StringJoiner のようなコレクションでないオブジェクトもコンテナ・オブジェクトと言えるでしょう。

全要素を走査して値を計算し返すメソッドは、言語によっては inject()*1, foldLeft(), reduceLeft*2, reduce()*3 などのように、別の名前が付けられていることが多いので注意が必要です。*4

さて、java.util.stream.Collector インターフェースを実装するには、まず Collector インターフェースがどんなものかを知らないといけませんね。 Stream<T> 型のストリームに適用でき、R 型のオブジェクトを返す Collector<T, R> インターフェースはこんなの:

package java.util.stream;

public interface Collector<T,R>{

    Set<Collector.Characteristics> characteristics();

    Supplier<R> resultSupplier();
    BiFunction<R,T,R> accumulator();
    BinaryOperator<R> combiner();

    public static enum Characteristics{
        CONCURRENT,
        STRICTLY_MUTATIVE,
        UNORDERED
    }
}
  • サブインターフェースの Collector.Characteristics 定数(列挙型)は以前の記事で見たコレクタの特性です。 characteristics() メソッドでは、これらの特性のうち実装しているコレクタが持っている特性を Set として返します。 実装する際は java.util.EnumSet などを使うといいでしょう。 コンテナ・オブジェクトを返すコレクタは特性として STRICTLY_MUTATIVE を返すようにしておいた方がいい(しておかないといけない)でしょう。
  • resultSupplier() メソッドは、値を返すコレクタでは初期値を、コンテナ・オブジェクトを返すコレクタでは空コンテナを生成します
  • accumulator() メソッドは「R apply(R, T)」というシグニチャのメソッドを持つ Function<R, T, R> オブジェクトを返します:
    • 値を返すコレクタでは、それまでに計算した値(R 型)と次の要素(T 型)を受け取り、新たな値(R 型)を計算する
    • コンテナ・オブジェクトを返すコレクタでは、コンテナ・オブジェクト(R 型)と次の要素(T 型)を受け取り、コンテナ・オブジェクト(R 型)を返す
  • combiner() メソッドは「R apply(R, R)」というシグニチャのメソッドを持つ BinaryOperator<T> オブジェクトを返します。
    • 値を返すコレクタでは、別のグループ内で計算された結果値(R 型)を受け取り、それらをまとめた計算値(R 型)を返す
    • コンテナ・オブジェクトを返すコレクタでは、別々のグループ内で構築されたコンテナ・オブジェクト(R 型)を受け取り、それらを連結したコンテナ・オブジェクトを返す

CONCURRENT なコレクタがコンテナ・オブジェクトを返す場合、accumulator(), combiner() メソッドに対して第1引数と返り値が同じオブジェクトでなければならないようです。

ちなみに JavaDoc によると、コレクタの処理は

BiFunction<R,T,R> accumulator = collector.accumulator();
R result = collector.resultSupplier().get();
for (T t : data)
    result = accumulator.apply(result, t);
return result;

と等価だそうです。 ここには combiner() メソッドは現れてませんが。

ではサンプルコード。 と言っても、いまいちイイ例が思い浮かばなかったので、StringJoiner による文字列の連結を行うコレクタ(Collectors.toStringJoiner() で返されるコレクタの劣化版)を作ってみようと思います。 まぁ、文字列を連結する処理はほとんど java.util.StringJoiner まかせなんですが。

public class StringJoinerCollector implements Collector<String, StringJoiner>{
    	
    public Set<Collector.Characteristics> characteristics(){
    	return EnumSet.of(STRICTLY_MUTATIVE);
    }
    
    public Supplier<StringJoiner> resultSupplier(){
        return () -> new StringJoiner(", ");
    }
    	
    public BiFunction<StringJoiner, String, StringJoiner> accumulator(){
    	return (sj, s) -> sj.add(s);
    }
        
    public BinaryOperator<StringJoiner> combiner(){
    	return (sj0, sj1) -> sj0.add(sj1);
    }
}

このコレクタを使用するには

// sequential ストリーム
Stream<String> stream0 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
System.out.println(stream0.collect(new StringJoinerCollector()));
    // 「Java, Groovy, Scala, Clojure, Kotlin, Jython, JRuby」と表示
System.out.println();

// parallel ストリーム
Stream<String> stream1 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
System.out.println(stream1.parallel().collect(new StringJoinerCollector()));
    // 「Java, Groovy, Scala, Clojure, Kotlin, Jython, JRuby」と表示

のようにします。 sequential ストリーム、parallel ストリーム、どちらに対しても上手く動くハズです。 どちらの場合も元のストリームの順序が保存されています。 ちなみに、もし上記の StringJoinerCollector の実装で combiner() メソッドが null を返すように実装した場合、sequential ストリームに対しては普通に動きますが、parallel ストリームに対して実行すると NullPoinerException が投げられます。 これは前回見た結果と一致しますね。

そういえば、StringJoinerCollector の実装で accumulator(), combiner() の各メソッドでラムダ式を直接返す実装にしてますが、メソッドを返すたびに新たにオブジェクトが生成されたりはしないようなのでご心配なく。

プログラミングGROOVY

プログラミングGROOVY

Scalaスケーラブルプログラミング第2版

Scalaスケーラブルプログラミング第2版

*1:Groovy

*2:Scala

*3:Clojure

*4:これらの言語では collect() メソッドがコンテナ・オブジェクト(もっと狭くコレクション・オブジェクト)を返す場合に限られてます。