倭マン's BLOG

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

蒐集してやんよ java.util.stream.Collectors クラス (6) - 独自コレクタを作る必要はない!? -

Java の Collectors クラスに定義されているメソッドを見ていくシリーズ(目次)。 前回、簡単な独自コレクタを作ってみましたが、作ったコレクタをあちこちで使い回したりしないなら、ラムダ式と Stream インターフェースに定義されているメソッドで事足りるようです。

コレクタの種類は大別して「値を返す」ものと「コンテナ・オブジェクトを返す」ものがあることを前回見ましたが、それぞれ

  • 値を返す・・・Stream#reduce() メソッド
  • コンテナ・オブジェクト・・・Stream#collect() メソッド

を使えば同様の機能を簡単に使うことができます。 もちろん、コレクタの accumulator や combiner が簡単な(1行の)ラムダ式で書けないとか、いろいろな場所で同様のコレクタを使うとかなら、クラスとして抽出するのも OK ですが。

値を返すコレクタ ・・・reduce() メソッド

値を返すコレクタとして、文字列のストリームから全文字数を計算するコレクタを作ってみましょう。

public static class StringLengthCollector implements Collector<String, Integer>{
    	
    public Set<Collector.Characteristics> characteristics(){
        return Collections.emptySet();
    }
    
    public Supplier<Integer> resultSupplier(){
        return () -> 0;
    }
    	
    public BiFunction<Integer, String, Integer> accumulator(){
        return (i, s) ->  i + s.length();
        // return (i, s) -> { System.out.println(s); return i + s.length(); };
    }
        
    public BinaryOperator<Integer> combiner(){
        return (i, j) ->  i + j;
        //return (i, j) ->  { System.out.println("combine!"); return i + j; };
    }
}

これを使うには

Stream<String> stream0 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
assert stream0.collect(new StringLengthCollector()) == 39;

Stream<String> stream1 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby").parallel();
assert stream1.collect(new StringLengthCollector()) == 39;

これを reduce() メソッドを使って書いてみましょう。 使う reduce() のシグニチャ

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)

です。 Collector#resultSupplier() の代わりに reduce() メソッドの第1引数に初期値を渡す以外は同じです:

Stream<String> stream2 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
assert stream2.reduce(0, (i, s) -> i + s.length(), (i, j) -> i + j) == 39;

Stream<String> stream3 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby").parallel();
assert stream3.reduce(0, (i, s) -> i + s.length(), (i, j) -> i + j) == 39;

コンテナ・オブジェクトを返すコレクタ・・・collect() メソッド

コンテナ・オブジェクトを返すコレクタは前回作った、StringJoinerCollector を使い回しましょう。

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);
    }
}

これを使うには

Stream<String> stream4 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
System.out.println(
    stream4.collect(new StringJoinerCollector()));
        // 「Java, Groovy, Scala, Clojure, Kotlin, Jython, JRuby」と表示

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

これを collect() メソッドを使って書いてみましょう。 使う collect() のシグニチャ

<R> R collect(Supplier<R> resultFactory,
              BiConsumer<R,? super T> accumulator,
              BiConsumer<R,R> combiner)

です。 コンテナ・オブジェクトを返す場合、accumulator や combiner が返り値を返す必要がない(第1引数が collect() メソッドの返り値のコンテナ・オブジェクトになる)ので、collect() メソッドの引数が BiFunction, BinaryOperator から BiConsumer に変更されています。 まぁ、ラムダ式で書く場合はあんまり違いを意識する必要はありませんが。 上記のサンプルを collect() メソッドで書くと以下のようになります:

Stream<String> stream2 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
System.out.println(
    stream2.collect(() -> new StringJoiner(", "), (sj, s) -> sj.add(s), (sj0, sj1) -> sj0.add(sj1.toString())));
        // 「Java, Groovy, Scala, Clojure, Kotlin, Jython, JRuby」と表示

Stream<String> stream3 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby").parallel();
System.out.println(
    stream2.collect(() -> new StringJoiner(", "), (sj, s) -> sj.add(s), (sj0, sj1) -> sj0.add(sj1.toString())));
        // 「Java, Groovy, Scala, Clojure, Kotlin, Jython, JRuby」と表示

reduce(), collect() メソッド、どちらで書き換える場合もラムダ式を複数書かないといけないので、複雑になりそうならコレクタをクラスとして抽出する方が無難かもしれませんね。 あと、今回はコレクタの特性はあまり気にしていませんでしたが、並列実行やパフォーマンスなどの点から特性を指定する必要があるならコレクタ・クラスを実装すべきでしょう。

プログラミングGROOVY

プログラミングGROOVY

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

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