グルーピングは「バラバラの一覧を“意味のあるかたまり”に整理する」技
グルーピングは、ざっくり言うと
「一覧を、あるキーごとにまとめ直す」ことです。
売上一覧を「店舗ごと」にまとめる。
ユーザー一覧を「都道府県ごと」にまとめる。
注文一覧を「ステータスごと」にまとめる。
こういう「集計前の整理」を、コードでやるのがグルーピングです。
Java では主に Map<キー, List<元データ>> の形で表現します。
一番基本のグルーピング:Stream + groupingBy
例:ユーザーを「都道府県ごと」にまとめる
まずは、典型的な例からいきます。
class User {
private final String name;
private final String prefecture;
public User(String name, String prefecture) {
this.name = name;
this.prefecture = prefecture;
}
public String getName() { return name; }
public String getPrefecture() { return prefecture; }
}
Javaこのユーザー一覧を、「都道府県ごと」にまとめたいとします。
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GroupingBasic {
public static void main(String[] args) {
List<User> users = List.of(
new User("山田", "東京都"),
new User("佐藤", "大阪府"),
new User("鈴木", "東京都")
);
Map<String, List<User>> byPref =
users.stream()
.collect(Collectors.groupingBy(User::getPrefecture));
System.out.println(byPref.get("東京都").size()); // 2
System.out.println(byPref.get("大阪府").size()); // 1
}
}
Javaここでの重要ポイントは二つです。
一つ目は、groupingBy(User::getPrefecture) が
「ユーザーを都道府県ごとにまとめる」という意味を、そのまま表していること。
二つ目は、結果の型が Map<String, List<User>> になっていること。
「キー(都道府県)→ その都道府県に属するユーザー一覧」という構造になっています。
グルーピング結果をどう使うかをイメージする
「都道府県ごとの人数」を出す
グルーピングした結果は、「集計」の前段階として使うことが多いです。
例えば、「都道府県ごとの人数」を出したい場合。
Map<String, List<User>> byPref = users.stream()
.collect(Collectors.groupingBy(User::getPrefecture));
for (Map.Entry<String, List<User>> e : byPref.entrySet()) {
String pref = e.getKey();
int count = e.getValue().size();
System.out.println(pref + " : " + count + "人");
}
Javaここでの感覚として大事なのは、
「グルーピングは“集計しやすい形に並べ替える”処理」だということです。
いきなり集計しようとすると if 文だらけになりますが、
一度グルーピングしてしまえば、
「グループごとにループして、好きな集計をする」だけで済みます。
便利な応用:グルーピングしながら集計までやる
groupingBy + counting で「件数マップ」を作る
実は groupingBy は、「グルーピングしたあとに何をするか」も一緒に指定できます。
よく使うのが counting() との組み合わせです。
import java.util.Map;
import java.util.stream.Collectors;
Map<String, Long> countByPref =
users.stream()
.collect(Collectors.groupingBy(
User::getPrefecture,
Collectors.counting()
));
Javaこれで、Map<String, Long>(都道府県 → 人数)が一発で作れます。
ここでの重要ポイントは、
「groupingBy(キー, 集計方法) という形で、“グルーピング+集計”を一度に書ける」ことです。
他にも、合計や平均など、いろいろな集計と組み合わせられます。
例:商品を「カテゴリごとの合計金額」にまとめる
class Item {
private final String category;
private final int price;
public Item(String category, int price) {
this.category = category;
this.price = price;
}
public String getCategory() { return category; }
public int getPrice() { return price; }
}
Javaこれを「カテゴリごとの合計金額」にしたい場合。
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
List<Item> items = List.of(
new Item("食品", 100),
new Item("食品", 200),
new Item("雑貨", 300)
);
Map<String, Integer> sumByCategory =
items.stream()
.collect(Collectors.groupingBy(
Item::getCategory,
Collectors.summingInt(Item::getPrice)
));
System.out.println(sumByCategory); // {食品=300, 雑貨=300}
Javaここでのポイントは、
「groupingBy(カテゴリ, summingInt(価格)) という一行が、
“カテゴリごとに価格を合計する”という業務仕様をそのまま表している」ことです。
グルーピングをユーティリティに閉じ込める
「毎回 Stream 書きたくない」を解消する
業務で何度も出てくるグルーピングは、
ユーティリティメソッドにしてしまうとスッキリします。
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public final class Groupings {
private Groupings() {}
public static <T, K> Map<K, List<T>> groupBy(
List<T> source,
Function<? super T, ? extends K> classifier
) {
if (source == null || source.isEmpty()) {
return Map.of();
}
return source.stream()
.collect(Collectors.groupingBy(classifier));
}
}
Java使い方はこうです。
Map<String, List<User>> byPref =
Groupings.groupBy(users, User::getPrefecture);
Javaここでの重要ポイントは、
「groupBy(users, User::getPrefecture) という呼び出しだけで、
“都道府県ごとにグルーピングする”という意図が伝わる」ことです。
Stream の細かい書き方を毎回思い出さなくてよくなり、
コードレビューでも「何をしたいのか」に集中できます。
複数キーでグルーピングしたいときの考え方
「都道府県 × 性別」でグルーピングする
業務では、「都道府県ごと」だけでなく、
「都道府県 × 性別ごと」など、複数条件でグルーピングしたいことがあります。
一番シンプルなのは、「複合キー」を作る方法です。
class PrefGenderKey {
private final String prefecture;
private final String gender;
public PrefGenderKey(String prefecture, String gender) {
this.prefecture = prefecture;
this.gender = gender;
}
public String getPrefecture() { return prefecture; }
public String getGender() { return gender; }
// equals / hashCode を必ず実装する(IDEに生成させる)
}
Javaこれをキーにしてグルーピングします。
Map<PrefGenderKey, List<User>> byPrefAndGender =
users.stream()
.collect(Collectors.groupingBy(
u -> new PrefGenderKey(u.getPrefecture(), u.getGender())
));
Javaここでの重要ポイントは、
「複数条件グルーピングは、“複合キー”という一つのキーにまとめてしまうとシンプルになる」ことです。
Map<String, Map<String, List<User>>> のような入れ子構造にする方法もありますが、
まずは「複合キーで一段の Map」にする方が扱いやすい場面も多いです。
null を含むデータをグルーピングするときの注意点
キーが null の場合をどう扱うか決める
groupingBy は、キーが null でも動きますが、Map のキーに null を入れることになるので、
「null をどう扱うか」は設計として決めておいた方がよいです。
例えば、「都道府県が未設定のユーザーは "UNKNOWN" にまとめる」など。
Map<String, List<User>> byPref =
users.stream()
.collect(Collectors.groupingBy(
u -> u.getPrefecture() == null ? "UNKNOWN" : u.getPrefecture()
));
Javaここでのポイントは、
「グルーピングの前に“キーを正規化する”」という発想です。
null をそのままキーにするのか、
特別な値に置き換えるのか、
そもそも除外するのか。
このルールを groupingBy の中で明示しておくと、
後から読んだ人にも意図が伝わります。
まとめ:グルーピングユーティリティで身につけてほしい感覚
グルーピングは、
単に「Map に詰め替えるテクニック」ではなく、
「集計しやすい形に一覧を整理し直す」ための基礎技術です。
一覧を Map<キー, List<元データ>> に変換するイメージを持つ。groupingBy(キー抽出関数) で「何ごとにまとめるか」を素直に書く。groupingBy(キー, counting / summing など) で「グルーピング+集計」を一気に書けることを覚える。
よく使うパターンはユーティリティメソッドにして、「何でグルーピングしているか」が名前から分かるようにする。
複数条件や null の扱いは、「キーをどう設計するか」でコントロールする。
あなたのコードの中に、
「同じキーを if や Map 操作で手作業でまとめている」部分があれば、
それを一度 groupingBy ベースのグルーピングに置き換えられないか眺めてみてください。
その小さな一歩が、
「一覧を“意味のあるかたまり”に整理してから考えられるエンジニア」への、
確かなステップになります。
