はじめに
題名が仰々しいですが、Collectors.groupingByが凄いぞって話しなだけです。
最近、groupingByを使って様々なMapを作成するケースが多くありました(なぜかMapを作成するケースは少なかったので、余り使用していませんでした)。
本記事はその時に調査した内容についてまとめたものとなります。
環境
Java10
Lombok
簡単なキーでgroupingBy
Collectors.groupingByはキーを与えれば楽に集約してくれるのがとても良い点です。
MapのValueを変更したい場合は引数をもう1つ増やしてそこに書けば良いため、様々なケースでの集約に対応しています。
List<Integer> nums = List.of(1,2,3,4,5,6,7,8,9,1,1,1);
Map<Integer,List<Integer>> map = nums.stream().collect(Collectors.groupingBy(i ->i));
System.out.println(map);
=> {1=[1, 1, 1, 1], 2=[2], 3=[3], 4=[4], 5=[5], 6=[6], 7=[7], 8=[8], 9=[9]}
複数のキーでgroupingBy
しかし、groupingByの最大の特徴は複雑なキーを設定しても上手いことやってくれることにあります。
複数のキーを設定するのはとても簡単で、キーの指定時に複数のフィールドを渡すだけ実現できます。
見やすいように"-"をいれていますが、なくても大丈夫です。
今回のキーとしてはname1とname2をキーとして設定することとします。
public static void main(String[] arg) {
List<String> names = List.of("taro", "jiro", "saburo");
List<Name> nameList = names.stream().map(it -> new Name(it, it, it)).collect(Collectors.toList());
Map<String,List<Name>> map = nameList.stream()
.collect(Collectors.groupingBy(it -> it.getName1() + "-" + it.getName2()));
System.out.println(map);
// => {jiro-jiro=[Main.Name(name1=jiro, name2=jiro, name3=jiro)], saburo-saburo=[Main.Name(name1=saburo, name2=saburo, name3=saburo)], taro-taro=[Main.Name(name1=taro, name2=taro, name3=taro)]}
}
@Data
private static class Name {
private String name1;
private String name2;
private String name3;
public Name(String name1, String name2, String name3) {
this.name1 = name1;
this.name2 = name2;
this.name3 = name3;
}
}
これだけではname3も同じ値が入っているため、name1とname2でキーとなっているかが分かりません。
そこでnew Name("taro", "jiro", "taro")
をListに追加してどのようなマップが出来るかを見てみたいと思います。
public static void main(String[] arg) {
List<String> names = List.of("taro", "jiro", "saburo");
List<Name> nameList = names.stream().map(it -> new Name(it, it, it)).collect(Collectors.toList());
nameList.add(new Name("taro", "jiro", "taro"));
Map<String,List<Name>> map = nameList.stream()
.collect(Collectors.groupingBy(it -> it.getName1() + "-" + it.getName2()));
System.out.println(map);
// => {jiro-jiro=[Main.Name(name1=jiro, name2=jiro, name3=jiro)], saburo-saburo=[Main.Name(name1=saburo, name2=saburo, name3=saburo)], taro-taro=[Main.Name(name1=taro, name2=taro, name3=taro)], taro-jiro=[Main.Name(name1=taro, name2=jiro, name3=taro)]}
}
結果を見ていただければ分かりますが、今回追加したObjectがtaro-jiro=[Main.Name(name1=taro, name2=jiro, name3=taro)]
として独立していますので、name1とname2がキーとなったことが分かります。
ここで気になるのが、結果のname3には"taro"と入っていることです。
groupingByの凄い所はここで、キーに指定した以外のフィールドの値については他のフィールドと同じでも上手いことやってくれることにあります。
本当かどうかを検証するため、name1とname3をキーにして実行したいと思います。
public static void main(String[] arg) {
List<String> names = List.of("taro", "jiro", "saburo");
List<Name> nameList = names.stream().map(it -> new Name(it, it, it)).collect(Collectors.toList());
nameList.add(new Name("taro", "jiro", "taro"));
Map<String,List<Name>> map = nameList.stream()
.collect(Collectors.groupingBy(it -> it.getName1() + "-" + it.getName3()));
System.out.println(map);
// => {jiro-jiro=[Main.Name(name1=jiro, name2=jiro, name3=jiro)], saburo-saburo=[Main.Name(name1=saburo, name2=saburo, name3=saburo)], taro-taro=[Main.Name(name1=taro, name2=taro, name3=taro), Main.Name(name1=taro, name2=jiro, name3=taro)]}
}
実行結果を確認すると見事、new Name("taro", "jiro", "taro")
として追加したObjectがtaro-taro=[Main.Name(name1=taro, name2=taro, name3=taro), Main.Name(name1=taro, name2=jiro, name3=taro)]
となり想定通りtaro-taro
のキーのValueとして登録されています。
複数キーはどこまでいけるのか
順番を入れ替えてみた
name1とname3の例をもう1パターン見てましょう。
フィールドまで見てくれているのは1つ前の例で見れましたが、name1とname3を入れ替えた例も見てみます。
public static void main(String[] arg) {
List<String> names = List.of("taro", "jiro", "saburo");
List<Name> nameList = names.stream().map(it -> new Name(it, it, it)).collect(Collectors.toList());
nameList.add(new Name("taro", "jiro", "taro"));
nameList.add(new Name("jiro", "taro", "taro"));
Map<String,List<Name>> map = nameList.stream()
.collect(Collectors.groupingBy(it -> it.getName1() + "-" + it.getName3()));
System.out.println(map);
// => {jiro-jiro=[Main.Name(name1=jiro, name2=jiro, name3=jiro)], saburo-saburo=[Main.Name(name1=saburo, name2=saburo, name3=saburo)], jiro-taro=[Main.Name(name1=jiro, name2=taro, name3=taro)], taro-taro=[Main.Name(name1=taro, name2=taro, name3=taro), Main.Name(name1=taro, name2=jiro, name3=taro)]}
}
順番を入れ替えてもきちんと対応してくれるようです。
Integerで試してみた
先ほどの例をInteger型でやってみました。
public static void main(String[] arg) {
List<Integer> nums = List.of(1,2,3);
List<Name> nameList = nums.stream().map(it -> new Name(it, it, it)).collect(Collectors.toList());
nameList.add(new Name(1, 2, 1));
nameList.add(new Name(2, 1, 1));
Map<Integer,List<Name>> map = nameList.stream()
.collect(Collectors.groupingBy(it -> it.getName1() + it.getName2()));
System.out.println(map);
}
@Data
private static class Name {
private Integer name1;
private Integer name2;
private Integer name3;
public Name(Integer name1, Integer name2, Integer name3) {
this.name1 = name1;
this.name2 = name2;
this.name3 = name3;
}
}
この場合はキーの計算結果によって集約されるようです。
キーが同じものが複数あったらおかしいので、納得できます。
Integer型でやってみた2
計算結果で集約されるようですが、念のためフィールドで見てくれるか確認します。
public static void main(String[] arg) {
List<Integer> nums = List.of(1,2,3);
List<Name> nameList = nums.stream().map(it -> new Name(it, it, it)).collect(Collectors.toList());
nameList.add(new Name(1, 1, 2));
nameList.add(new Name(1, 2, 1));
Map<Integer,List<Name>> map = nameList.stream()
.collect(Collectors.groupingBy(it -> it.getName1() + it.getName3()));
System.out.println(map);
// => 2=[Main.Name(name1=1, name2=1, name3=1), Main.Name(name1=1, name2=2, name3=1)], 3=[Main.Name(name1=1, name2=1, name3=2)], 4=[Main.Name(name1=2, name2=2, name3=2)], 6=[Main.Name(name1=3, name2=3, name3=3)]}
}
@Data
private static class Name {
private Integer name1;
private Integer name2;
private Integer name3;
public Name(Integer name1, Integer name2, Integer name3) {
this.name1 = name1;
this.name2 = name2;
this.name3 = name3;
}
}
Integer型でも見てくれました。
Integer型でやってみた3
Integer型のお試し3つ目です。
今度はキーを文字列に変換してそれをキーにした場合を考えてみます。
public static void main(String[] arg) {
List<Integer> nums = List.of(1,2,3);
List<Name> nameList = nums.stream().map(it -> new Name(it, it, it)).collect(Collectors.toList());
nameList.add(new Name(1, 2, 1));
nameList.add(new Name(2, 1, 1));
Map<String,List<Name>> map = nameList.stream()
.collect(Collectors.groupingBy(it -> it.getName1() + "-" + it.getName2()));
System.out.println(map);
// => {1-1=[Main.Name(name1=1, name2=1, name3=1)], 2-1=[Main.Name(name1=2, name2=1, name3=1)], 1-2=[Main.Name(name1=1, name2=2, name3=1)], 2-2=[Main.Name(name1=2, name2=2, name3=2)], 3-3=[Main.Name(name1=3, name2=3, name3=3)]}
}
@Data
private static class Name {
private Integer name1;
private Integer name2;
private Integer name3;
public Name(Integer name1, Integer name2, Integer name3) {
this.name1 = name1;
this.name2 = name2;
this.name3 = name3;
}
}
この場合は、順番も考慮されるようです。
キーを3つ指定してみる
さらにキーを増やしても問題ないか確認してみます。
public static void main(String[] arg) {
List<String> names = List.of("taro", "jiro", "saburo");
List<Name> nameList = names.stream().map(it -> new Name(it, it, it)).collect(Collectors.toList());
nameList.add(new Name("taro","taro","jiro"));
nameList.add(new Name("taro","jiro","taro"));
Map<String,List<Name>> map = nameList.stream()
.collect(Collectors.groupingBy(it -> it.getName1() + "-" + it.getName2() + "-" + it.getName3()));
System.out.println(map);
// => {jiro-jiro-jiro=[Main.Name(name1=jiro, name2=jiro, name3=jiro)], taro-jiro-taro=[Main.Name(name1=taro, name2=jiro, name3=taro)], taro-taro-taro=[Main.Name(name1=taro, name2=taro, name3=taro)], taro-taro-jiro=[Main.Name(name1=taro, name2=taro, name3=jiro)], saburo-saburo-saburo=[Main.Name(name1=saburo, name2=saburo, name3=saburo)]}
}
@Data
private static class Name {
private String name1;
private String name2;
private String name3;
public Name(String name1, String name2, String name3) {
this.name1 = name1;
this.name2 = name2;
this.name3 = name3;
}
}
キーを増やしても大丈夫なようです。
まとめ
もうちょっと色々やってみようかなと思いましたが、傾向がつかめたのでここでgoupingByの動作についてまとめて終わろうと思います。
基本的にはキーに指定したフィールドをLambda式に従って作成し、その値で集約するという動作をするメソッドとなるようです。
String型であれば指定した順番とフィールドが連動してキーになっているのが分かり、数値型であれば指定したフィールドの計算結果がキーになっているため作成した結果が同じであれば分かります。
さいごに
goupingByは本当に強力ということが分かりました。
Collectorsにある他のメソッドと組み合わせるとさらに強くなれるのですが、別の機会としたいと思います(機会があるかは分かりません)。
ちなみに今回の記事にあたってJava内部の処理を見ていないので、なぜこのような結果になるかは分かってません!