Java

Java データを特定のキーごとにグルーピングしたMapを作成する方法

Javaで特定のキーごとにグルーピングしたMapを作成する方法を解説します。

例えば社員クラス[Employee]のデータが複数存在するとして、その社員クラスが持つ組織ごとにグルーピングしたMapを作成したいときなどを想定しています。

具体的には以下のような変換をする方法を解説していきます。

List<Employee> → Map<String, Employee>

準備

社員クラスである、Employeeは以下のクラスです。一般的なクラスです。

class Employee {

    private Employee() {
    }

    public Employee(String department, String name, int age) {
        this.department = department;
        this.name = name;
        this.age = age;
    }

    private String department;
    private String name;
    private int age;

    public String getDepartment() {
        return this.department;
    }

    public String getName() {
        return this.name;
    }

    public int getAge() {
        return this.age;
    }

    @Override
    public String toString() {
        return String.format("組織:%s 名前:%s 年齢:%d歳", this.department, this.name, this.age);
    }
}

これをList<Employee>の形式で使用したのが、以下です。

public class Test {
    public static void main(String[] args) {
        List<Employee> employeeList = new ArrayList<>();
        Employee employee1 = new Employee("経理部", "山田晃子", 45);
        Employee employee2 = new Employee("経理部", "須藤珠緒", 69);
        Employee employee3 = new Employee("営業部", "県直人", 23);

        employeeList.add(employee1);
        employeeList.add(employee2);
        employeeList.add(employee3);

        employeeList.stream().forEach(System.out::println);
        
    }
}

結果は以下のようになります。

組織:経理部 名前:山田晃子 年齢:45歳
組織:経理部 名前:須藤珠緒 年齢:69歳
組織:営業部 名前:県直人 年齢:23歳

グルーピングする

例えば、組織ごとにデータを処理したい場合に、組織ごとにデータをグルーピングしたいというケースが存在します。

以下のような結果になると嬉しい場面があります。

経理部:
{組織:経理部 名前:山田晃子 年齢:45歳,
 組織:経理部 名前:須藤珠緒 年齢:69歳},
営業部:
{組織:営業部 名前:県直人 年齢:23歳}

これを再現するために、Map<String, List<Employee>>の型にデータをグルーピングするというのが本記事の目的です。

Mapのキーには組織、バリューにはキーの組織の社員のListを設定します。

やり方は色々ありますが、今回はスマートなやり方を2パターンご紹介します。

MapのcomputeIfAbsentを使用する

1つ目の方法はMapクラスのcomputeIfAbsentを使用する方法です。

この方法では、for文を使用するため、stream()を何らかの理由で使用できない場合に有効です。※プロジェクトのメンバーがstream()を知らない人が多いなどの理由で使えないことがあるそうです。。。

以下のコードで実現できます。Employeeクラスは同じです。

public class Test {
    public static void main(String[] args) {

        // もととなるリストを生成する
        List<Employee> employeeList = new ArrayList<>();
        Employee employee1 = new Employee("経理部", "山田晃子", 45);
        Employee employee2 = new Employee("経理部", "須藤珠緒", 69);
        Employee employee3 = new Employee("営業部", "県直人", 23);

        employeeList.add(employee1);
        employeeList.add(employee2);
        employeeList.add(employee3);

        // パターン① computeIfAbsentを使用する。
        Map<String, List<Employee>> groupsOne = new HashMap<>();
        for (Employee employee : employeeList) {
            groupsOne.computeIfAbsent(employee.getDepartment(),
                (unused) -> new ArrayList<>()).add(employee);
        }
        System.out.println(groupsOne);
    }
}

結果は以下のようになります。

{経理部=[組織:経理部 名前:山田晃子 年齢:45歳, 組織:経理部 名前:須藤珠緒 年齢:69歳],
 営業部=[組織:営業部 名前:県直人 年齢:23歳]}

期待通りの結果が取得できました。

computeIfAbsentは第1引数の値がキーに存在する場合は、groupsOneからそのキーのバリューを返却します。

キーに存在しない場合は、第1引数の値をキー、第2引数の結果をバリューとしてgroupsOneに追加し、そのバリューを返却します。なお、第2引数がnullである場合は、何も行われません。

以下の流れで処理が実行されます。

  1. 1つ目のEmployee:groupsOneに経理部が存在しないため、キーが経理部かつバリューが空のリストをgroupsOneに追加する。また、そのバリューに対して、1つ目のemployeeを追加する。
  2. 2つ目のEmployee:groupsOneに経理部が存在するため、キーが経理部のリストに対して、2つ目のemployeeを追加する。
  3. 3つ目のEmployee:groupsOneに営業部が存在しないため、キーが営業部かつバリューが空のリストをgroupsOneに追加する。また、そのバリューに対して、3つ目のemployeeを追加する。

そこそこスマートにグルーピングできました。

stream()の終端処理でCollectors.groupingByを使用する

stream()を使用してもいい場合は、終端処理でCollectors.groupingByを使用すると、よりスマートに処理を記述することができます。

以下のコードで実現できます。Employeeクラスは同じです。

public class Test {
    public static void main(String[] args) {

        // もととなるリストを生成する
        List<Employee> employeeList = new ArrayList<>();
        Employee employee1 = new Employee("経理部", "山田晃子", 45);
        Employee employee2 = new Employee("経理部", "須藤珠緒", 69);
        Employee employee3 = new Employee("営業部", "県直人", 23);

        employeeList.add(employee1);
        employeeList.add(employee2);
        employeeList.add(employee3);

        // パターン② stream()の終端処理でCollectors.groupingByを使用する。
        Map<String, List<Employee>> groupsTwo = employeeList
            .stream()
            .collect(Collectors.groupingBy(Employee::getDepartment));
        System.out.println(groupsTwo);
    }
}

こちらも結果は同じになります。

{経理部=[組織:経理部 名前:山田晃子 年齢:45歳, 組織:経理部 名前:須藤珠緒 年齢:69歳],
 営業部=[組織:営業部 名前:県直人 年齢:23歳]}

ポイントはstreamの終端処理collectでCollectors.groupingByを使用しているところです。

Collectors.groupingByの引数にEmployeeクラスのグルーピングしたい項目(今回は組織)を取得する処理を渡すだけでOKです。

ソースを比較していただければ分かる通り、非常にシンプルでstream()に慣れていれば、可読性も高い記述になっています。

特殊な理由がない限りはstream()を使用してグルーピングすることをオススメします。

まとめ

Javaでデータクラスを特定のキーごとにグルーピングする場合は、以下の方法を使用するといいでしょう。

  • MapのcomputeIfAbsentを使用する
  • stream()の終端処理でCollectors.groupingByを使用する

どちらも結果は同様になるので、プロジェクトの慣習などに合わせて選択しましょう。

【お知らせ 無料!】未経験エンジニアがJavaでWebサイトを作成できるようになるための学習ロードマップを、無料で公開しています!

実体験に基づいて作成されているので、プログラミングスクールなどで指導されるロードマップにも劣らない品質です。

こちらの「【Java】エンジニア未経験者がJavaを効率的に勉強する手順を紹介します」リンクから無料で閲覧できるので、是非ご覧ください!

【2021/7 更新】エンジニア未経験者がJavaを効率的に勉強する手順を紹介します【学習ロードマップ】プログラミングを勉強する際に候補に出てくる言語に Java があります。 最近は Java 以外の言語である、Ruby や Pyt...