倭マン's BLOG

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

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

Java の Collectors クラスに定義されているメソッドを見ていくシリーズ(目次)。 前回に引き続き、今回は Map を返す Collector を見ていきます。 前回にも増して引数の型パラメータを見てるとクラクラするぅ。 まぁ、前回同様、返り値の Collector の第2型パラメータあたりを中心に見ておけばいいんじゃないかなぁ。 今回扱うのはこの部分が Map もしくはそれを拡張した型になってます。

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

//***** toMap() 関連 *****
// toMap()
static <T,K,U> Collector<T,Map<K,U>>
toMap(Function<? super T,? extends K> keyMapper,
      Function<? super T,? extends U> valueMapper)

static <T,K,U> Collector<T,Map<K,U>>
toMap(Function<? super T,? extends K> keyMapper,
      Function<? super T,? extends U> valueMapper,
      BinaryOperator<U> mergeFunction)

static <T,K,U,M extends Map<K,U>> Collector<T,M>
toMap(Function<? super T,? extends K> keyMapper,
      Function<? super T,? extends U> valueMapper,
      BinaryOperator<U> mergeFunction,
      Supplier<M> mapSupplier)

// toConcurrentMap()
static <T,K,U> Collector<T,ConcurrentMap<K,U>>
toConcurrentMap(Function<? super T,? extends K> keyMapper,
                Function<? super T,? extends U> valueMapper)

static <T,K,U> Collector<T,ConcurrentMap<K,U>>
toConcurrentMap(Function<? super T,? extends K> keyMapper,
                Function<? super T,? extends U> valueMapper,
                BinaryOperator<U> mergeFunction)

static <T,K,U,M extends ConcurrentMap<K,U>> Collector<T,M>
toConcurrentMap(Function<? super T,? extends K> keyMapper,
                Function<? super T,? extends U> valueMapper,
                BinaryOperator<U> mergeFunction,
                Supplier<M> mapSupplier)

// Merger
static <T> BinaryOperator<T> firstWinsMerger()
static <T> BinaryOperator<T> lastWinsMerger()
static <T> BinaryOperator<T> throwingMerger()

//***** 分割・グループ化 *****
// partitioningBy()
static <T> Collector<T,Map<Boolean,List<T>>>
partitioningBy(Predicate<? super T> predicate)

static <T,D> Collector<T,Map<Boolean,D>>
partitioningBy(Predicate<? super T> predicate,
               Collector<? super T,D> downstream)

// groupingBy()
static <T,K> Collector<T,Map<K,List<T>>>
groupingBy(Function<? super T,? extends K> classifier)

static <T,K,D> Collector<T,Map<K,D>>
groupingBy(Function<? super T,? extends K> classifier,
           Collector<? super T,D> downstream)

static <T,K,D,M extends Map<K,D>> Collector<T,M>
groupingBy(Function<? super T,? extends K> classifier,
           Supplier<M> mapFactory,
           Collector<? super T,D> downstream)

// groupingByConcurrent()
static <T,K> Collector<T,ConcurrentMap<K,List<T>>>
groupingByConcurrent(Function<? super T,? extends K> classifier)

static <T,K,D> Collector<T,ConcurrentMap<K,D>>
groupingByConcurrent(Function<? super T,? extends K> classifier,
                     Collector<? super T,D> downstream)

static <T,K,D,M extends ConcurrentMap<K,D>> Collector<T,M>
groupingByConcurrent(Function<? super T,? extends K> classifier,
                     Supplier<M> mapFactory,
                     Collector<? super T,D> downstream)

xxxConcurrent() は並行実行する以外は xxx() というメソッドと変わらないと思うので省略。

toMap() メソッド

まずは toMap() メソッド。 3つのオーバーロードされたシグニチャがあります:

static <T,K,U> Collector<T,Map<K,U>>
toMap(Function<? super T,? extends K> keyMapper,
      Function<? super T,? extends U> valueMapper)

static <T,K,U> Collector<T,Map<K,U>>
toMap(Function<? super T,? extends K> keyMapper,
      Function<? super T,? extends U> valueMapper,
      BinaryOperator<U> mergeFunction)

static <T,K,U,M extends Map<K,U>> Collector<T,M>
toMap(Function<? super T,? extends K> keyMapper,
      Function<? super T,? extends U> valueMapper,
      BinaryOperator<U> mergeFunction,
      Supplier<M> mapSupplier)

引数として2つの Function をとるメソッドは各要素からマップのキーと値を生成して Map オブジェクトを構築するというメソッド。 これはまぁ基本というか素直なメソッドですね。 更に BinaryOperator 型の引数をとるメソッドはキーとして同じオブジェクトが現れたときに値をどうするか?を指定します。 これはもちろんどんな BinaryOperator オブジェクトでもいいんですが、通常は Collectors クラスに定義されている static メソッド

static <T> BinaryOperator<T> firstWinsMerger()    // 先に現れた方を優先
static <T> BinaryOperator<T> lastWinsMerger()    // 後に現れた方を優先
static <T> BinaryOperator<T> throwingMerger()    // IllegalStateException を投げる(同じキーが現れることを許さない)

のいずれかを指定することが多いのではないかと。 更に更に Supplier オブジェクトを指定するメソッドは、返される Map の実装として何を用いるのかを指定します。 では簡単なサンプルコード:

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

// toMap(keyMapper, valueMapper)
Stream<String> stream0 = Stream.of("Java", "Groovy", "Scala", "Clojure");
Map<String, Integer> lengthMap = stream0.collect(toMap(s -> s, s -> s.length()));
    // (文字列, 文字列の長さ) のペアのマップ
assert lengthMap.get("Java") == 4;
assert lengthMap.get("Scala") == 5;

// toMap(keyMapper, valueMapper, merger)
Stream<String> stream1 = Stream.of("Java", "Groovy", "Groovy", "Scala", "Java", "Clojure", "Java");
Map<String, Integer> countMap = stream1.collect(toMap(s -> s, s -> 1, (i, j) -> i + j));    // merger 使ってない・・・
    // (文字列, 文字列の出現数) のペアのマップ
assert countMap.get("Java") == 3;
assert countMap.get("Clojure") == 1;

// toMap(keyMapper, valueMapper, merger, mapSupplier)
Stream<String> stream2 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
Map<String,String> headMap = stream2.collect(toMap(s->s.substring(0, 1), s->s, firstWinsMerger(), TreeMap::new));
    // (頭文字, 文字列) のペアのマップ
assertString(headMap.get("J"), "Java");
assertString(headMap.get("G"), "Groovy");

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

第3引数の BinaryOperator には Collections の static メソッドを使った merger を使うことが多いのでは、と言っておいて上記のサンプルでは使ってないというこの天の邪鬼さw 3つ目で使ってるので許してネ。 3つ目で firstWinsMerger() の代わりに lastWinsMerger() を用いると、headMap.get("J") の返り値は "JRuby" となります。 throwingMerger() の場合は IllegalStateException が投げられます。

要素をそのままキー or 値に使いたい場合に「s -> s」と書いてますが、JavaDoc ではこの恒等変換を「Functions.identity()」と書いてあるところがあります。 ただし、悲しいことに Functions というクラスが存在しないんだが・・・ 昔のビルドの名残かな。

第4引数の Map の実装を指定する箇所では、コンストラクタの参照「《クラス名》::new」を使うのがよろしいようで。 型パラメータの指定がなくても(付けられないけど)怒られない模様。

partioningBy() メソッド

partitioningBy() メソッドは、ストリームの各要素を「ある命題」に対して true を返すものと false を返すものに分割(グループ分け)するメソッドです。 数学っぽく言えば、「ある命題」に対する真理集合とその補集合に分割すると言ってもいいかと*1。 「ある命題」は java.util.stream.Predicate インターフェースのオブジェクトで表します*2。 Collector の返り値の Map はキーとして Boolean 型の値(つまりは true / false)をとります。 オーバーロードされた2つの partitioningBy() メソッドのシグニチャは以下の通り:

static <T> Collector<T,Map<Boolean,List<T>>>
partitioningBy(Predicate<? super T> predicate)

static <T,D> Collector<T,Map<Boolean,D>>
partitioningBy(Predicate<? super T> predicate,
               Collector<? super T,D> downstream)

2つ目のメソッドの第2引数の Collector は、true (or false) に分類された要素(群)をどのようにマップの値として保持するかを指定します。 この Collector を指定しない1つ目の partitioningBy() メソッドでは、true (or false) に分類された要素をまとめて List オブジェクトに保存し、それをマップの値としています。 ではサンプルコード:

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

// partitioningBy(predicate)
Stream<String> stream3 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
Map<Boolean,List<String>> jstartMap1 = stream3.collect(partitioningBy(s->s.startsWith("J")));
    // "J" で始まるかどうかで分割
assertStringList(jstartMap1.get(true), "Java", "Jython", "JRuby");
assertStringList(jstartMap1.get(false), "Groovy", "Scala", "Clojure", "Kotlin");

// partitioningBy(prediate, collector)
Stream<String> stream4 = Stream.of("Java", "Groovy", "Scala", "Clojure", "Kotlin", "Jython", "JRuby");
Map<Boolean,StringJoiner> jstartMap2 = stream4.collect(partitioningBy(s->s.startsWith("J"), toStringJoiner("/")));
    // "J" で始まる(or 始まらない)文字列を "/" で連結して StringJoiner として保持
assertString(jstartMap2.get(true), "Java/Jython/JRuby");
assertString(jstartMap2.get(false), "Groovy/Scala/Clojure/Kotlin");

/** 第1引数の文字列リストと第2引数の文字列配列をリストにしたものをアサーション */
public static void assertStringList(List<String> list, String... ss){
    assert list.equals(Arrays.asList(ss));
}

/** 第1引数の StringJoiner を toString() で文字列にして、第2引数の String とアサーション */
public static void assertString(StringJoiner sj, String s){
    assertString(sj.toString(), s);
}

前回やった mapping() や reducing() は、マップの値として単なるコレクションではなく、何らかの変換や計算をしたものを使いたい場合に重宝するようです。 まぁ、コレクションでも Set にしたいときには toSet() を使ったりもできるし、その他どんな Collector でも使えますけどね。

groupingBy() メソッド

groupingBy() メソッドは partitionigBy() メソッドと同じようにストリームの要素をグループ分けしますが、partitioningBy() のように true/false の2つに分割するのではなく、もっと多くのグループに分けます。 先ほどの例の「"J" で始まるかどうか」ではなく、「頭文字によってグループ化」という感じになります。 数学で言うと商集合を作るって感じですかね。 商集合を作る同値関係は groupingBy() の第1引数に渡された Function を用いてストリームの各要素を変換した結果得られるオブジェクトの equals() による同値関係・・・でしょうね。 まぁ、そういうのはさておき、groupingBy() のオーバーロードされたシグニチャは3つ:

static <T,K> Collector<T,Map<K,List<T>>>
groupingBy(Function<? super T,? extends K> classifier)

static <T,K,D> Collector<T,Map<K,D>>
groupingBy(Function<? super T,? extends K> classifier,
           Collector<? super T,D> downstream)

static <T,K,D,M extends Map<K,D>> Collector<T,M>
groupingBy(Function<? super T,? extends K> classifier,
           Supplier<M> mapFactory,
           Collector<? super T,D> downstream)

2つ目の第2引数、3つ目の第3引数として渡される Collector は partitioningBy() の Collector と同じく、同一グループに分類された要素(群)をマップの値としてどのように保持するかを指定します。 この Collector を指定する必要のない1つ目の groupingBy() は単なるリストとして要素群を保持します。 3つ目の第2引数に渡される Supplier はマップの実装を指定します。 ではサンプルコード:

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

// groupingBy(classifier)
Stream<String> stream5 = Stream.of(
        "Java", "Groovy", "Scala", "Clojure", "Jython", "JRuby", "Go", "Smalltalk", "C++", "C#");
Map<Character, List<String>> langDic1 = stream5.collect(groupingBy(s -> s.charAt(0)));
    // 頭文字でグループ化
assertStringList(langDic1.get('J'), "Java", "Jython", "JRuby");
assertStringList(langDic1.get('G'), "Groovy", "Go");

// groupingBy(classifier, collector)
Stream<String> stream6 = Stream.of(
        "Java", "Groovy", "Scala", "Clojure", "Jython", "JRuby", "Go", "Smalltalk", "C++", "C#");
Map<Character,StringJoiner> langDic2 = stream6.collect(groupingBy(s->s.charAt(0), toStringJoiner(" ")));
    // 頭文字でグループ化。 グループ内の文字列はスペース " " で連結して StringJoiner として保持
assertString(langDic2.get('S'), "Scala Smalltalk");
assertString(langDic2.get('C'), "Clojure C++ C#");

// groupingBy(classifier, mapFactory, collector)
Stream<String> stream7 = Stream.of(
        "Java", "Groovy", "Scala", "Clojure", "Jython", "JRuby", "Go", "Smalltalk", "C++", "C#");
Map<Character,Set<String>> langDic3 = stream7.collect(groupingBy(s->s.charAt(0), TreeMap::new, toSet()));
    // 頭文字でグループ化。 Map の実装として TreeMap を使用しよう
assert langDic3 instanceof TreeMap;
assert langDic3.get('J').size() == 3;

/** 第1引数の文字列リストと第2引数の文字列配列をリストにしたものをアサーション */
public static void assertStringList(List<String> list, String... ss){
    assert list.equals(Arrays.asList(ss));
}

/** 第1引数 StringJoiner を toString() で文字列にして、第2引数の String とアサーション */
public static void assertString(StringJoiner sj, String s){
    assertString(sj.toString(), s);
}

基本的には partitioningBy() と使い方は変わりませんが、こちらの方がよく使いそう。

さて、これで Collectors クラスに定義されているメソッドは1通り見終わりました。 次回は独自 Collector を作ろうかな。 全く関係ないことやるかも。

オブジェクト指向プログラマが次に読む本 ?Scalaで学ぶ関数脳入門

オブジェクト指向プログラマが次に読む本 ?Scalaで学ぶ関数脳入門

*1:ストリームに含まれている要素すべての集合を全体集合として。

*2:prediate は「述語」の意。 Predicate インターフェースは boolean 値を返す抽象メソッド test() を持ちます。