LoginSignup
20
23

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-09-21

はじめに

題名が仰々しいですが、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内部の処理を見ていないので、なぜこのような結果になるかは分かってません!

20
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
23