Java Tips | コレクション:グルーピング

Java Java
スポンサーリンク

グルーピングは「バラバラの一覧を“意味のあるかたまり”に整理する」技

グルーピングは、ざっくり言うと
「一覧を、あるキーごとにまとめ直す」ことです。

売上一覧を「店舗ごと」にまとめる。
ユーザー一覧を「都道府県ごと」にまとめる。
注文一覧を「ステータスごと」にまとめる。

こういう「集計前の整理」を、コードでやるのがグルーピングです。
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 ベースのグルーピングに置き換えられないか眺めてみてください。

その小さな一歩が、
「一覧を“意味のあるかたまり”に整理してから考えられるエンジニア」への、
確かなステップになります。

タイトルとURLをコピーしました