後世まで残したいCollectors.groupingByの話

# はじめに
題名が仰々しいですが、Collectors.groupingByが凄いぞって話しなだけです。

最近、groupingByを使って様々なMapを作成するケースが多くありました(なぜかMapを作成するケースは少なかったので、余り使用していませんでした)。
本記事はその時に調査した内容についてまとめたものとなります。

# 環境
Java10
Lombok

# 簡単なキーでgroupingBy
Collectors.groupingByはキーを与えれば楽に集約してくれるのがとても良い点です。
MapのValueを変更したい場合は引数をもう1つ増やしてそこに書けば良いため、様々なケースでの集約に対応しています。

``` Java
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をキーとして設定することとします。

``` Java
    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に追加してどのようなマップが出来るかを見てみたいと思います。

``` Java
    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をキーにして実行したいと思います。

``` Java
    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を入れ替えた例も見てみます。

``` Java
    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型でやってみました。

``` Java
    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
計算結果で集約されるようですが、念のためフィールドで見てくれるか確認します。

``` Java
    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つ目です。
今度はキーを文字列に変換してそれをキーにした場合を考えてみます。

``` Java
    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つ指定してみる
さらにキーを増やしても問題ないか確認してみます。

``` Java
    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内部の処理を見ていないので、なぜこのような結果になるかは分かってません！