倭マン's BLOG

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

蒐集してやんよ java.util.stream.Collectors クラス (1) - Map 以外を返す Collector -

今回から何回かに分けて java.util.stream パッケージに定義されている Collectors クラスに定義されているメソッドを見ていきます。

目次

  1. Map 以外を返す Collector
  2. Map を返す Collector
  3. 内蔵コレクタの特性
  4. parallel ストリーム と concurrent コレクタ
  5. 独自コレクタを作ってみよう!
  6. 独自コレクタを作る必要はない!?

Collector インターフェースと Collectors クラス

Collectors クラスはいわゆるユーティリティ・クラスで、インスタンス生成やクラス拡張はできず、static メソッドのみが定義されています。 これらの static メソッドはほとんどが java.util.stream.Collector オブジェクトを返すメソッドで*1、よく使うであろうコレクターが定義されてます。

Collector インターフェースは Stream インターフェースの collect() メソッドに引数として渡して使います。 Collector インターフェースは型パラメータが2つ宣言されていて

package java.util.stream;

public interface Collector<T,R>{ ... }

T はストリームの要素の型(つまり Stream<T> オブジェクトの collect() メソッドに渡せる)、R は collect() メソッドの返り値の型になります。 今回は返り値が Map でないコレクターを見ていきます:

今回扱うメソッドは以下のもの:

// 汎用コレクター
static <T,U,R> Collector<T,R>
mapping(Function<? super T,? extends U> mapper,
        Collector<? super U,R> downstream)

static <T> Collector<T,T>
reducing(BinaryOperator<T> op)

static <T> Collector<T,T>
reducing(T identity,
         BinaryOperator<T> op)

static <T,U> Collector<T,U>
reducing(U identity,
         Function<? super T,? extends U> mapper,
         BinaryOperator<U> op)

// 文字列への変換
static Collector<String,StringBuilder>
toStringBuilder()

static Collector<CharSequence,StringJoiner>
toStringJoiner(CharSequence delimiter)

// コレクションへの変換
static <T,C extends Collection<T>> Collector<T,C>
toCollection(Supplier<C> collectionFactory)

static <T> Collector<T,List<T>>
toList()

static <T> Collector<T,Set<T>>
toSet()

// 要約統計量 Summary Statistics
static <T> Collector<T,Long>
counting()

static <T> Collector<T,Long>
sumBy(Function<? super T,Long> mapper)

static <T> Collector<T,T>
maxBy(Comparator<? super T> comparator)

static <T> Collector<T,T>
minBy(Comparator<? super T> comparator)

static <T> Collector<T,IntSummaryStatistics>
toIntSummaryStatistics(ToIntFunction<? super T> mapper)

static <T> Collector<T,LongSummaryStatistics>
toLongSummaryStatistics(ToLongFunction<? super T> mapper)

static <T> Collector<T,DoubleSummaryStatistics>
toDoubleSummaryStatistics(ToDoubleFunction<? super T> mapper)

引数の型の型パラメータまで見てると億劫になるので、とりあえず返り値の Collector の第2型パラメータが、Stream#collect() メソッドに Collector を渡した後の返り値だっ、てところをチェックしておけばいいんじゃないかなぁ。

汎用コレクター

まずはある程度汎用的に使える Collector を返すメソッド:

static <T,U,R> Collector<T,R>
mapping(Function<? super T,? extends U> mapper,
        Collector<? super U,R> downstream)

static <T> Collector<T,T>
reducing(BinaryOperator<T> op)

static <T> Collector<T,T>
reducing(T identity,
         BinaryOperator<T> op)

static <T,U> Collector<T,U>
reducing(U identity,
         Function<? super T,? extends U> mapper,
         BinaryOperator<U> op)

Stream インターフェースに定義されている map() や reduce() と何が違うの?という気もしますが、JavaDoc を見ていると、次回やる groupingBy(), partitioningBy() メソッドのうち Collector オブジェクトを引数にとるものに対して使うといいようです。 まぁ、今回はあまり気にせずに普通に使ってみましょう:

import static java.util.stream.Collectors.*;

// mapping() 各文字列を、その長さの int 値に変換
Stream<String> stream0 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
List<Integer> lengthList = stream0.collect(mapping(s -> s.length(), toList()));
assert lengthList.get(2) == "Scala".length();

// reducing() その1 文字列を連結
Stream<String> stream1 = Stream.of("Java", "Groovy", "Scala", "Clojure");
assertString(stream1.collect(reducing((s0, s1) -> s0 + s1)), "JavaGroovyScalaClojure");

// reducing() その2
Stream<String> stream2 = Stream.of("Java", "Groovy", "Scala", "Clojure");
assertString(stream2.collect(reducing("*", (s0, s1) -> s0 + s1)), "*JavaGroovyScalaClojure");

// reducing() その3
Stream<String> stream3 = Stream.of("Java", "Groovy", "Scala", "Clojure");
assert stream3.collect(reducing(0, s -> s.length(), (i, j) -> i + j)) == 22;

/** 文字列のアサーション */
public static void assertString(String s0, String s1){
    assert s0.equals(s1);
}

reducing() のうち、1つ目、2つ目のものはストリームの要素の型と返り値の型が同じになってます。 また、引数が BinaryOperator<T> オブジェクトを1つだけとる reducing() の返り値は T ですが、Stream<T> インターフェースの reduce() メソッドは Optional<T> であるという違いもあります。

文字列への変換

次は文字列に関するコレクターを返すメソッド:

static Collector<String,StringBuilder>
toStringBuilder()

static Collector<CharSequence,StringJoiner>
toStringJoiner(CharSequence delimiter)

java.util.StringJoiner クラスは Java 8 から導入された新たなクラスで、複数の文字列 (CharSequence) を指定した文字列(デリミター)で連結して返すメソッドです。 使い方はどちらも同じようなものですが、返り値が String オブジェクトではないことに注意:

import static java.util.stream.Collectors.*;

// StringBuilder を構築
Stream<String> stream4 = Stream.of("Java", "Groovy", "Scala", "Clojure");
assertString(stream4.collect(toStringBuilder()).toString(),
             "JavaGroovyScalaClojure");

// StringJoiner を構築
Stream<String> stream5 = Stream.of("Java", "Groovy", "Scala", "Clojure");
assertString(stream5.collect(toStringJoiner(", ")).toString(),
             "Java, Groovy, Scala, Clojure);

/** 文字列のアサーション */
public static void assertString(String s0, String s1){
    assert s0.equals(s1);
}

たぶん StringJoiner を使うコレクターは結構デバッグなので多用することになるかと。

コレクションへの変換

次はストリームをコレクションに変換するコレクター。 List, Set に変換するコレクターと、具象クラスを指定してコレクションを構築するコレクターがあります:

static <T,C extends Collection<T>> Collector<T,C>
toCollection(Supplier<C> collectionFactory)

static <T> Collector<T,List<T>>
toList()

static <T> Collector<T,Set<T>>
toSet()

使い方は一番簡単かと:

import static java.util.stream.Collectors.*;

// Collection の構築 (TreeSet)
Stream<String> stream6 = Stream.of("Java", "Groovy", "Scala", "Clojure");
NavigableSet<String> naviset = stream6.collect(toCollection(TreeSet::new));
assert naviset instanceof NavigableSet;
assertString(naviset.stream().collect(toStringJoiner("")), "ClojureGroovyJavaScala");

// List の構築
Stream<String> stream7 = Stream.of("Java", "Groovy", "Scala", "Clojure");
List<String> list = stream7.collect(toList());
assert list.size() == 4;
assert list instanceof ArrayList;

// Set の構築
Stream<String> stream8 = Stream.of("Java", "Groovy", "Groovy", "Scala", "Java", "Clojure", "Java");
Set<String> set = stream8.collect(toSet());
assert set.size() == 4;
assert set instanceof HashSet;
set.forEach(e -> { assert stream().anyMatch(f -> e.equals(f)); });

/** 文字列のアサーション */
public static void assertString(Object s0, String s1){
    assert s0.toString().equals(s1);
}

まぁ、List と Set で大抵はなんとかなるかな。 toList(), toSet() で生成されるオブジェクトの型をアサーションしてますが、これは実装によるかと。

要約統計量

要約統計量(summary statistics 代表値)は和とか平均とかのことですが、プリミティブ型ストリームの記事で同様のメソッドを扱いましたね。 ここでは同じことを、数値に変換する方法や Comparator オブジェクトとともに指定する感じです。 counting() は単に要素の個数を数えるだけですが:

static <T> Collector<T,Long>
counting()

static <T> Collector<T,Long>
sumBy(Function<? super T,Long> mapper)

static <T> Collector<T,T>
maxBy(Comparator<? super T> comparator)

static <T> Collector<T,T>
minBy(Comparator<? super T> comparator)

static <T> Collector<T,IntSummaryStatistics>
toIntSummaryStatistics(ToIntFunction<? super T> mapper)

static <T> Collector<T,LongSummaryStatistics>
toLongSummaryStatistics(ToLongFunction<? super T> mapper)

static <T> Collector<T,DoubleSummaryStatistics>
toDoubleSummaryStatistics(ToDoubleFunction<? super T> mapper)

使い方もプリミティブ型ストリームのときと同じです:

import static java.util.stream.Collectors.*;

// counting()
Stream<String> stream9 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
assert stream9.collect(counting()) == 7;

// sumBy()
Stream<String> stream10 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
assert stream10.collect(sumBy(s -> Long.valueOf(s.length()))) == 39;

// maxBy()
Stream<String> stream11 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
assertString(stream11.collect(maxBy((s0, s1) -> s0.length() - s1.length())), "Clojure");

// IntSummaryStatistics の構築
Stream<String> stream12 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
IntSummaryStatistics stat = stream12.collect(toIntSummaryStatistics(s -> s.length()));
assert stat.getCount() == 7;
assert stat.getSum() == 39;
assertDouble(stat.getAverage(), 39.0/7.0);

次回は返り値が Map のコレクターを見ていきます。

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

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

*1:次回扱う、merger をあらわす BinaryOperator を返すメソッド以外。