「groupingBy 複数キー」でやりたいことのイメージ
Collectors.groupingBy の「複数キー」は、ざっくり言うと
「1 つの条件じゃなくて、2 個以上の条件の組み合わせでグループ分けしたい」
という話です。
例えば「部署 × 性別ごとに社員をグループ分けしたい」「年 × 言語ごとに本をグループ分けしたい」みたいなやつですね。引用元: Java 8 Streams – Collectors.groupingBy() を使用した複数フィールドによるグループ化
やり方は大きく 2 パターンあります。
ひとつは「groupingBy をネストする」パターン。
もうひとつは「複数のフィールドを 1 つの“複合キー”にまとめる」パターンです。
順番にかみ砕いていきます。
パターン1:groupingBy をネストして「Map の中に Map」を作る
部署 × 性別でグループ分けする例
まずは一番ストレートなやり方から。
社員クラスをざっくりこう決めます。
class Employee {
private final String name;
private final String department; // 部署
private final String gender; // "M" / "F" など
Employee(String name, String department, String gender) {
this.name = name;
this.department = department;
this.gender = gender;
}
String getDepartment() { return department; }
String getGender() { return gender; }
String getName() { return name; }
@Override
public String toString() {
return name;
}
}
Javaこれを「部署ごと」かつ「その中で性別ごと」にグループ分けしたいとします。
コードはこう書けます。
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GroupingByNested {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Alice", "Sales", "F"),
new Employee("Bob", "Sales", "M"),
new Employee("Carol", "Dev", "F"),
new Employee("Dave", "Dev", "M"),
new Employee("Ellen", "Dev", "F")
);
Map<String, Map<String, List<Employee>>> grouped =
employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment, // 第1キー:部署
Collectors.groupingBy(Employee::getGender) // 第2キー:性別
));
System.out.println(grouped);
}
}
Java型に注目すると、
Map<部署, Map<性別, List<Employee>>>
という形になっています。
つまり、外側の Map が「部署ごと」、内側の Map が「性別ごと」、一番内側が「そのグループに属する社員のリスト」です。引用元: Javaのラムダ式(Stream API)で複数キーでCollectors.groupingByする
ネストが増えるとどうなるか
同じノリで「部署 × 性別 × 雇用形態」みたいに増やしていくと、
Collectors.groupingBy(
Employee::getDepartment,
Collectors.groupingBy(
Employee::getGender,
Collectors.groupingBy(Employee::getType)
)
)
Javaのように、groupingBy を入れ子にしていくことになります。
型は
Map<部署, Map<性別, Map<雇用形態, List<Employee>>>>
という、なかなかパンチのある形になります。
「階層構造として扱いたい」「部署 → 性別 → … と段階的に見たい」なら、このネスト方式は素直で分かりやすいです。
一方で、「ただ“組み合わせ”でグループ分けしたいだけ」のときは、もう少しフラットに扱いたくなることもあります。
パターン2:複数フィールドを 1 つの「複合キー」にまとめる
「部署+性別」を 1 つのキーとして扱う
もうひとつの考え方は、
「部署」と「性別」を別々のキーにするのではなく、
「部署+性別」という 1 つのキーにしてしまう方法です。
そのために、小さなキー用クラス(または record)を作ります。
import java.util.Objects;
class DeptGenderKey {
private final String department;
private final String gender;
DeptGenderKey(String department, String gender) {
this.department = department;
this.gender = gender;
}
String getDepartment() { return department; }
String getGender() { return gender; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof DeptGenderKey)) return false;
DeptGenderKey other = (DeptGenderKey) o;
return Objects.equals(department, other.department)
&& Objects.equals(gender, other.gender);
}
@Override
public int hashCode() {
return Objects.hash(department, gender);
}
@Override
public String toString() {
return department + "-" + gender;
}
}
Javaこれをキーにして groupingBy します。
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GroupingByCompositeKey {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Alice", "Sales", "F"),
new Employee("Bob", "Sales", "M"),
new Employee("Carol", "Dev", "F"),
new Employee("Dave", "Dev", "M"),
new Employee("Ellen", "Dev", "F")
);
Map<DeptGenderKey, List<Employee>> grouped =
employees.stream()
.collect(Collectors.groupingBy(
e -> new DeptGenderKey(e.getDepartment(), e.getGender())
));
System.out.println(grouped);
}
}
Java型は
Map<DeptGenderKey, List<Employee>>
と、かなりスッキリします。
DeptGenderKey の中身を見れば、「どの部署のどの性別か」が一目で分かりますし、equals / hashCode をちゃんと実装しておけば、Map のキーとしても安全に使えます。引用元: Java 8 Streams – Collectors.groupingBy() を使用した複数フィールドによるグループ化
record を使うともっとシンプルになる
Java 16 以降なら、複合キーは record で書くとかなり楽です。
record DeptGenderKey(String department, String gender) {}
Javaこれだけで、equals / hashCode / toString まで自動生成されます。
あとは先ほどと同じように、
Map<DeptGenderKey, List<Employee>> grouped =
employees.stream()
.collect(Collectors.groupingBy(
e -> new DeptGenderKey(e.getDepartment(), e.getGender())
));
Javaと書くだけです。
どっちのパターンを選ぶべきかの設計感覚
ネスト Map 方式が向いている場面
「部署 → 性別 → …」のように、階層構造として扱いたいとき。
例えば、「まず部署ごとにループして、その中で性別ごとに処理したい」ようなケースです。
この場合、
Map<String, Map<String, List<Employee>>> grouped = ...;
for (var entryDept : grouped.entrySet()) {
String dept = entryDept.getKey();
Map<String, List<Employee>> byGender = entryDept.getValue();
for (var entryGender : byGender.entrySet()) {
String gender = entryGender.getKey();
List<Employee> list = entryGender.getValue();
// 部署×性別ごとの処理
}
}
Javaのように、段階的に降りていくコードが自然に書けます。
複合キー方式が向いている場面
「単に“組み合わせ”でグループ分けしたいだけ」で、
階層構造として扱う必要はあまりないとき。
例えば、「部署×性別ごとの人数を集計して一覧にしたい」ようなケースでは、
Map<DeptGenderKey, Long> counts =
employees.stream()
.collect(Collectors.groupingBy(
e -> new DeptGenderKey(e.getDepartment(), e.getGender()),
Collectors.counting()
));
Javaのように書けて、とてもフラットで扱いやすくなります。
Map.Entry<DeptGenderKey, Long> をそのまま List に変換して画面表示、みたいな流れも作りやすいです。
さらに一歩:複数キー+下流 Collector の組み合わせ
「部署×性別ごとの人数」を出す
さっき少し触れたように、groupingBy の第 2 引数に「下流 Collector」を渡すと、
「グループごとにさらに集計する」ことができます。引用元: 【java8】streamのgroupingbyの使い方
複合キー方式で人数を数える例をもう少し丁寧に書くと、こうです。
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GroupingCountDemo {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Alice", "Sales", "F"),
new Employee("Bob", "Sales", "M"),
new Employee("Carol", "Dev", "F"),
new Employee("Dave", "Dev", "M"),
new Employee("Ellen", "Dev", "F")
);
Map<DeptGenderKey, Long> counts =
employees.stream()
.collect(Collectors.groupingBy(
e -> new DeptGenderKey(e.getDepartment(), e.getGender()),
Collectors.counting()
));
counts.forEach((key, count) ->
System.out.println(key + " : " + count));
}
}
Java出力イメージはこんな感じです。
Sales-F : 1
Sales-M : 1
Dev-F : 2
Dev-M : 1
「複数キーでグループ分け」+「グループごとの集計」というのは、
実務でかなり頻出のパターンなので、groupingBy(複合キー, counting()) の形はぜひ手に馴染ませておくといいです。
まとめ:groupingBy 複数キーを自分の言葉で整理する
あなたの言葉でまとめるなら、こうなります。
「groupingBy の複数キーは、groupingBy をネストして Map<キー1, Map<キー2, List<T>>> のような階層構造にするか、
複数フィールドを 1 つの“複合キー”クラス(record)にまとめて Map<複合キー, List<T>> にするか、
どちらかのパターンで実現する。」
そして設計のポイントは、
階層として扱いたいならネスト方式
フラットに扱いたい・集計したいなら複合キー方式
複合キーを作るときは equals / hashCode を正しく実装する(record だと楽)groupingBy(キー, counting() / summingInt() / mapping() ...) のように、下流 Collector と組み合わせると一気に表現力が上がる
あたりです。
