2018年4月17日火曜日

[JavaSE8 Goldへの道] その10 Collectorsクラスを使ったストリームのリダクション操作

JavaSE8 Goldへの道(Upgrade to Java SE 8 Programmer 1Z0-810 試験対策)10回目です。

一連の記事は「JavaSE8Gold」ラベルを付けていきます。

Stream#collectメソッドに渡すCollectorの基本的な実装を集めたCollectorsクラスについて、その6でも少しご紹介しましたが、まだ試験範囲から紹介できていないものがあるので今回はそれをご紹介します。


Collectorsクラスの各種メソッド

toXxx()

最も基本なのはtoList()toSet()メソッドでしょう。ストリームの要素をListSetに集約します。
当然ながら、toSet()の方は要素の重複があると1つになってしまいます。

//staticインポート staticメソッドをクラス名指定なしで記述できる
//以降の例は全て定義済みとする
import static java.util.stream.Collectors.*;
:
:
Set<String> set = Stream.of("a", "b", "a", "c", "d")
        .collect(toSet());

System.out.println(set);

実行結果
[a, b, c, d]

実装を見ると、返されるのはそれぞれArrayListHashSetのようです。
他のコレクションにしたい場合はtoCollection(Supplier<C>)メソッドを使います。
例えばコンテナとしてTreeSetを使いたい場合は以下のようにします。

Set<String> set = Stream.of("a", "b", "a", "c", "d")
                .collect(toCollection(TreeSet::new));

Supplierには使いたいクラスのインスタンスを生成してやればよく、コンストラクタ参照で記述できます。
ラムダ式で書けば() -> new TreeSet()です。

少々ややこしいのがtoMap()です。引数の違いで3種類ありますが、一番引数が少ないものの定義は以下のようになっています。

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

要するに、要素1つからキーと値を算出してあげなければなりません。
例えば社員クラスのストリームから社員IDをキーとしてマップに変換するには以下のようにします。

Map<String, Employee> map = Stream.of(new Employee("1", "アムロ"), new Employee("2", "カイ"), new Employee("3", "ハヤト"))
        .collect(toMap(e -> e.getId() , e -> e));
        //このようにも書ける
        //.collect(toMap(Employee::getId , Function.identity()));

System.out.println(map);

実行結果(※EmployeeにはtoStringが実装済み)
{1=アムロ, 2=カイ, 3=ハヤト}

コメント行のように書くこともできます。第一引数はメソッド参照、第二引数は要素それ自身を返せばいいのですが、e->eと書くのもいまいちなのでFunctionインタフェースに用意されているユーティリティメソッドFunction#identityが使えます。

このメソッドではキーが重複する場合IllegalStateExceptionがスローされます。
これを回避したい場合は引数を3つ取るtoMap()が使えます。第三引数にはキーが重複した場合に2つの値を合成するためのBinaryOperatorを渡します。上記例では合成しようがありませんが、例えば値が文字列の場合結合して1つの文字列にするといったことができます。

引数4つのものはMapの型を指定できます。

public 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)

public 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> mapFactory)

groupingBy

SQLのgroup byのようにグループ化を行います。基本はMap<K,List<T>>型として、キーごとに要素をリストに入れたものを生成します。

Map<String, List<Employee>> map = Stream.of(new Employee("地球連邦", "アムロ"), new Employee("地球連邦", "カイ"), new Employee("地球連邦", "ハヤト"),
                                        new Employee("ジオン公国", "シャア"), new Employee("ジオン公国", "ガトー"), new Employee("ジオン公国", "ラル"))
                                    .collect(groupingBy(Employee::getId));

System.out.println(map);
実行結果
{地球連邦=[アムロ, カイ, ハヤト], ジオン公国=[シャア, ガトー, ラル]}

集約方法をList以外にしたい場合、Mapを他の実装にしたい/別の集計にしたい場合は引数の異なるgroupingByが用意されているのでそちらが使えます。

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

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

downstreamに別のCollector(例えばtoSet())を指定することで別の集約が可能です。
Collectors#counting()を渡してグループの要素数をカウントするといったこともできます。


pertitoningBy

groupingByのより単純なもので、PredicateによってBoolean.TRUEBoolean.FALSEをキーに分割します。

Map<Boolean, List<Employee>> map = Stream.of(new Employee("地球連邦", "アムロ"), new Employee("地球連邦", "カイ"), new Employee("地球連邦", "ハヤト"),
                                        new Employee("ジオン公国", "シャア"), new Employee("ジオン公国", "ガトー"), new Employee("ジオン公国", "ランバ・ラル"))
                                        .collect(partitioningBy(e -> e.getId().equals("地球連邦")));

System.out.println(map);
実行結果
{false=[シャア, ガトー, ランバ・ラル], true=[アムロ, カイ, ハヤト]}

これも引数の異なるバージョンでList以外の集約も可能です。
public static <T,D,A> Collector<T,?,Map<Boolean,D>> partitioningBy (Predicate<? super T> predicate,
                                                                                                                         Collector<? super T,A,D> downstream)


averagingXxx

各プリミティブ型ごとにメソッドが用意されています。
要素に値変換関数(ToDoubleFunction等)を通した結果の平均を求めます。
double avg = Stream.of("A", "BB", "CCC", "DDDD", "EEEEE")
        .collect(averagingInt(s -> s.length()));

System.out.println(avg);
実行結果
3.0
結果の型は全てDoubleです。

その7で説明したプリミティブストリームを使っても同じ結果が得られますが、その場合戻り値はOptionalDoubleなのでもう一つ手順が必要になります。
double avg = Stream.of("A", "BB", "CCC", "DDDD", "EEEEE")
        .mapToInt(s -> s.length())
        .average()
        .getAsDouble(); //OptionalDoubleから値を取得

System.out.println(avg);
OptionalDoubleなのは要素が0だった場合に値なし(null)となるためですが、ではaveragingXxxの方でやってみると・・・
double avg = Stream.<String>empty() //型推論ができないので指定する必要あり
        .collect(averagingInt(s -> s.length()));

System.out.println(avg);
実行結果
0.0
となり0になりました。ちょっと統一取れてない感じです。

joining

要素がCharSequenceの場合に、すべての要素を結合して一つの文字列にします。

String str = Stream.of(new Employee("地球連邦", "アムロ"), new Employee("地球連邦", "カイ"), new Employee("地球連邦", "ハヤト"),
                    new Employee("ジオン公国", "シャア"), new Employee("ジオン公国", "ガトー"), new Employee("ジオン公国", "ラル"))
                    .map(e -> e.getName())
                    .collect(joining());

System.out.println(str);
実行結果
アムロカイハヤトシャアガトーラル

引数なしだとそのまま結合、引数1つだと区切り文字を指定できます。最後の要素の後はカンマなしに・・とか考えることなく、カンマ区切りテキストも簡単に生成できます。
3つの場合接頭辞と接尾辞が指定できます。引数の順番に注意です。

String str = Stream.of(new Employee("地球連邦", "アムロ"), new Employee("地球連邦", "カイ"), new Employee("地球連邦", "ハヤト"),
                    new Employee("ジオン公国", "シャア"), new Employee("ジオン公国", "ガトー"), new Employee("ジオン公国", "ラル"))
                    .map(e -> e.getName())
                    .collect(joining(",", "「", "」"));

System.out.println(str);
実行結果
「アムロ,カイ,ハヤト,シャア,ガトー,ラル」


と、い、う、わ、け、で

Collectorsクラスにはもう少しメソッドがあるのですが、試験範囲じゃないっぽいのでこの辺にしておきます。
興味のある方はAPIドキュメントを御覧ください。
今回も以下のサイトとGoldの通常試験の参考書を参考にしています。
一連の記事は「JavaSE8Gold」ラベルを付けていきます。

それではみなさまよきガジェットライフを(´∀`)ノ


▼こちらの記事もどうぞ

▼ブログを気に入っていただけたらRSS登録をお願いします!
▼ブログランキング参加中!応援よろしくお願いします。

スポンサーリンク