- Contents -
groupingBy の後の集約について
groupingByしたあとに、ただのListではなく、Setや、Map、さらにはLinkedHashMapにしたい場合もあるでしょう。
この記事では、groupingByしたあとに、それぞれの形に集約するサンプルをいくつか紹介していきます。
groupingByとは
groupingByとは、ストリームの集約関数でCollectorsというクラスに用意されています。
特定のキー値でStreamに流れてきた要素をグルーピングするための集約関数となります。
引数がいくつか省略できますが、キーだけを指定する場合、
集約された結果は、Map<K, List
このList
Map<K, List<V>>
デフォルト
サンプルデータとして、
下記リストの要素をグルーピングしてみます。
名前と年齢、性別を表現した要素のリストです。
List<Person> list = List.of(
new Person(12, "Rose", "♀"),
new Person(13, "Bobby", "♂"),
new Person(13, "Abby", "♀"),
new Person(14, "Lewis", "♂"),
new Person(14, "Hannah", "♀"),
new Person(15, "Dexter", "♂"),
new Person(16, "Andrew", "♂"),
new Person(17, "Noah", "♀"),
new Person(18, "Iris", "♀"),
new Person(18, "Tommy", "♂"),
new Person(21, "Thomas", "♂"),
new Person(25, "Leon", "♂"),
new Person(28, "Norris", "♂"),
new Person(20, "Emma", "♀"),
new Person(24, "Kara", "♀"),
new Person(28, "Vanessa", "♀"),
new Person(29, "Celia", "♀"),
new Person(33, "Ken", "♂"),
new Person(36, "Cody", "♂"),
new Person(36, "Mey", "♀"));
Collectors.groupingBy を使用して、年代ごとにグルーピングします。
// 年代ごとにグルーピングする。
// 10毎にグルーピング
Map<Integer, List<Person>> map = list.stream()
.collect(Collectors.groupingBy(o -> (o.age / 10) * 10));
デフォルトのgroupoingByでは、指定のキー毎に List<V>
が作られます。
10=[Rose(12:♀), Bobby(13:♂), Abby(13:♀), Lewis(14:♂), Hannah(14:♀), Dexter(15:♂), Andrew(16:♂), Noah(17:♀), Iris(18:♀), Tommy(18:♂)]
20=[Thomas(21:♂), Leon(25:♂), Norris(28:♂), Emma(20:♀), Kara(24:♀), Vanessa(28:♀), Celia(29:♀)]
30=[Ken(33:♂), Cody(36:♂), Mey(36:♀)]
donwstream
デフォルトのListから、Set,Mapなどに集約したい場合、
groupingByメソッドのdownstreamに、集約関数を指定します。
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)
downstreamに渡すのは、Collectors.toSet() Collectors.toMap() などのすでに用意されている関数を渡す事ができますので、
ここの関数をうまく調整すれば、目的に合わせていろいろできるわけです。
Map<K, Set<V>>
// 年代ごとに年齢のSetを作る
// downstreamに Collectors.toSet() を指定するだけ
Map<Integer, Set<Integer>> map = list.stream()
.map(o -> o.age)
.collect(Collectors.groupingBy(o -> (o / 10) * 10, Collectors.toSet()));
年齢がSet<Integer>
になっているのが確認できます。
10=[16, 17, 18, 12, 13, 14, 15]
20=[20, 21, 24, 25, 28, 29]
30=[33, 36]
Map<K, Map<L, V>>
// 年代ごとにnameでMapを作る
// downstreamに Collectors.toMap() を指定するだけ
// {年代 => {名前 => Person, ...}, ...}
Map<Integer, Map<String, Person>> map = list.stream()
.collect(Collectors.groupingBy(o -> (o.age / 10) * 10,
Collectors.toMap(o -> o.name, Function.identity())));
年代でグルーピングした後、nameをキーとしたHashMapになります。
10={Iris=Iris(18:♀), Andrew=Andrew(16:♂), Lewis=Lewis(14:♂), Abby=Abby(13:♀), Hannah=Hannah(14:♀), Rose=Rose(12:♀), Bobby=Bobby(13:♂), Dexter=Dexter(15:♂), Noah=Noah(17:♀), Tommy=Tommy(18:♂)}
20={Thomas=Thomas(21:♂), Norris=Norris(28:♂), Vanessa=Vanessa(28:♀), Kara=Kara(24:♀), Leon=Leon(25:♂), Emma=Emma(20:♀), Celia=Celia(29:♀)}
30={Mey=Mey(36:♀), Cody=Cody(36:♂), Ken=Ken(33:♂)}
Map<K, LinkedHashMap<L, V>>
前述したとおり、
Collectors.toMapを使えば、グルーピングした後にHashMap化することが可能です。
このHashMapをLinkedHashMapにしたい場合は、toMapの引数にさらにmapFactoryを指定するようにします。
Collectors.toMapのシグネチャはこんな感じ、mapFactoryは第4引数です。
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)
// 年代ごとにnameでMapを作る
// Collectors.toMapのmapFactoryに作成したいクラスのコンストラクタ参照を指定。
Map<Integer, LinkedHashMap<String, Person>> map = list.stream()
.collect(Collectors.groupingBy(o -> (o.age / 10) * 10,
Collectors.toMap(o -> o.name,
Function.identity(),
(a, b) -> {
throw new IllegalStateException("duplicated keys: " + a + " and " + b);
},
LinkedHashMap::new)));
前述のHashMapの場合と比べて、LinkedHashMapになっているので、
要素の並び順が追加された順番(もともとの要素の順)で保持されている事に注目。
(HashMapの場合、順序は保持されません)
10={Rose=Rose(12:♀), Bobby=Bobby(13:♂), Abby=Abby(13:♀), Lewis=Lewis(14:♂), Hannah=Hannah(14:♀), Dexter=Dexter(15:♂), Andrew=Andrew(16:♂), Noah=Noah(17:♀), Iris=Iris(18:♀), Tommy=Tommy(18:♂)}
20={Thomas=Thomas(21:♂), Leon=Leon(25:♂), Norris=Norris(28:♂), Emma=Emma(20:♀), Kara=Kara(24:♀), Vanessa=Vanessa(28:♀), Celia=Celia(29:♀)}
30={Ken=Ken(33:♂), Cody=Cody(36:♂), Mey=Mey(36:♀)}
Collectors.toMapのmergeFunctionについて
Collectors.toMapの第4引数:mapFactoryを指定したいがために、第3引数:mergeFunctionも指定する必要があります。
このmergeFunctionとは、
キーが重複した場合に、どちらの要素を採用するのかを決めるためにコールされるものです。
採用する方をreturnすれば良いのです。
そもそも、キーが重複することを想定していないような場合では、
下記のように例外を発生させるのが良いでしょう。
(toMapの引数を省略した場合の挙動がそうなっています)
Collectors.toMap(o -> o.name,
Function.identity(),
(a, b) -> {
// キー重複が発生した時点で例外にする
throw new IllegalStateException("duplicated keys: " + a + " and " + b);
},
LinkedHashMap::new)));
Map<K, TreeMap<L, V>>
最後にTreeMapですが、LinkedHashMapの場合と同じく、Collectors.toMapのmapFactoryを指定すればOKです。
// 年代ごとにnameでMapを作る
// Collectors.toMapのmapFactoryに作成したいクラスのコンストラクタの参照を指定する
Map<Integer, TreeMap<String, Person>> map = list.stream()
.collect(Collectors.groupingBy(o -> (o.age / 10) * 10,
Collectors.toMap(o -> o.name,
Function.identity(),
(a, b) -> {
throw new IllegalStateException("duplicated keys: " + a + " and " + b);
},
TreeMap::new)));
TreeMap<String, Person>
になるので、nameキーがアルファベット順に並んでいるのがわかると思います。
10={Abby=Abby(13:♀), Andrew=Andrew(16:♂), Bobby=Bobby(13:♂), Dexter=Dexter(15:♂), Hannah=Hannah(14:♀), Iris=Iris(18:♀), Lewis=Lewis(14:♂), Noah=Noah(17:♀), Rose=Rose(12:♀), Tommy=Tommy(18:♂)}
20={Celia=Celia(29:♀), Emma=Emma(20:♀), Kara=Kara(24:♀), Leon=Leon(25:♂), Norris=Norris(28:♂), Thomas=Thomas(21:♂), Vanessa=Vanessa(28:♀)}
30={Cody=Cody(36:♂), Ken=Ken(33:♂), Mey=Mey(36:♀)}
応用 groupingByで更に階層を深くする。
downstreamに集約関数を渡すことで、更に集約できるのがわかったと思います。
応用として、更にgroupingByすることで、階層を深くすることも可能です。
男女別でグルーピングした後に、
更に年代別にグルーピングした場合のサンプルも下記に記載します。
// 性別でグルーピング後、さらに年代ごとにnameでMapを作る
// {性別 => {年代 => {名前 => Person, ...}, ...}, ...}
Map<String, Map<Integer, TreeMap<String, Person>>> map = list.stream().collect(
Collectors.groupingBy(o -> o.sex,
Collectors.groupingBy(o -> (o.age / 10) * 10,
Collectors.toMap(o -> o.name,
Function.identity(),
(a, b) -> {
throw new IllegalStateException("duplicated keys: " + a + " and " + b);
},
TreeMap::new))));
downstreamに更に集約関数を続ける事で、更にネストすることが可能。
♀={
10={Abby=Abby(13:♀), Hannah=Hannah(14:♀), Iris=Iris(18:♀), Noah=Noah(17:♀), Rose=Rose(12:♀)}
20={Celia=Celia(29:♀), Emma=Emma(20:♀), Kara=Kara(24:♀), Vanessa=Vanessa(28:♀)}
30={Mey=Mey(36:♀)}
}
♂={
10={Andrew=Andrew(16:♂), Bobby=Bobby(13:♂), Dexter=Dexter(15:♂), Lewis=Lewis(14:♂), Tommy=Tommy(18:♂)}
20={Leon=Leon(25:♂), Norris=Norris(28:♂), Thomas=Thomas(21:♂)}
30={Cody=Cody(36:♂), Ken=Ken(33:♂)}
}
まとめ
- groupingByにdownstreamとして、更に集約関数を渡す事ができる。
- downstreamを指定していない場合、デフォルトでList
で集約される。 - Collectors.toSet, toMap を利用することでグルーピングして更に集約する方法を変更できる。
- 集約関数を組み合わせることで、好きに階層を作れる。
groupingByを使うと、複雑なMap構造を作れます。
変数宣言するのがちょっと面倒ですが、
Java9以降であれば var を使って型推論で省略して記載できるので便利です。
今回は集約後の型がわかりやすいようにあえて明記しています。
覚えると、いろんなパターンが自由にかけます。
書き方はちょっと忘れやすいと思うので、またその時はこの記事を参考にしてもらえると嬉しいです。