groupingBy をざっくり一言でいうと
Collectors.groupingBy は、
「Stream の要素を、ある“キー”ごとにグループ分けして Map にまとめる ための仕組み」
です。
イメージとしては、
「科目ごとに点数を集める」
「部署ごとに社員をまとめる」
「年齢ごとにユーザーを分ける」
といったときに、
Map<キー, List<要素>>
の形で「グループ別の一覧」を、一発で作ってくれる道具です。
for 文でやろうとすると、Map を用意して、キーがあるか調べて、なければ新しい List 作って…という、あの定番の面倒なパターンを全部肩代わりしてくれます。
まずは基本:List を「キーごと」に分ける例
例題:ユーザーを「年齢」でグループ分けする
シンプルな User クラスを用意します。
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + "(" + age + ")";
}
}
Javaこのユーザーを「年齢ごと」にグループ分けしてみましょう。
import java.util.*;
import java.util.stream.Collectors;
public class GroupingByBasic {
public static void main(String[] args) {
List<User> users = List.of(
new User("Alice", 20),
new User("Bob", 20),
new User("Carol", 30),
new User("Dan", 30),
new User("Eve", 40)
);
Map<Integer, List<User>> byAge =
users.stream()
.collect(Collectors.groupingBy(u -> u.age));
System.out.println(byAge);
}
}
JavagroupingBy(u -> u.age) のところがポイントです。
「User から“グループ分けに使うキー”を取り出す関数」を渡しています。
ここでは「年齢(int)」をキーにしています。
結果の byAge は、ざっくりこういうイメージの Map になります。
20 → [Alice(20), Bob(20)]
30 → [Carol(30), Dan(30)]
40 → [Eve(40)]
つまり、型としては Map<Integer, List<User>> です。
「キー:年齢」「値:その年齢のユーザー一覧」という形になっている、ということです。
groupingBy の「キー関数」をしっかり意識する
groupingBy のシグネチャをかみ砕く
一番シンプルな形のシグネチャは、こうです。
static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier)
Java難しそうに見えますが、噛み砕くと、
「T 型の要素を、classifier という関数でキー K に変換し、
そのキーごとに T を List<T> にまとめた Map<K, List<T>> を作る Collector」
という意味です。
Stream 側は Stream<T> です。
users.stream() → Stream<User>groupingBy(u -> u.age) → 「User から年齢 Integer を取り出す関数」
これを渡すことで、
Map<Integer, List<User>>
が手に入る、という構造です。
「何でグループ分けしたいか?」を先に決める
groupingBy を使うとき、最初に考えるべきは
「このデータを、何をキーにしてまとめたいのか?」
です。
例えばユーザーなら、年齢だったり、都道府県だったり、ステータスだったり、部署だったり。
「この項目ごとに分けるのが自然だよね」というものをキーにします。
その「キーを取り出す関数」が、
u -> u.ageu -> u.prefectureUser::getStatus
のような形になります。
もう少し実務寄りな例:ユーザーを都道府県ごとにまとめる
ユーザーに都道府県を持たせる
class User {
String name;
String prefecture;
User(String name, String prefecture) {
this.name = name;
this.prefecture = prefecture;
}
@Override
public String toString() {
return name + "(" + prefecture + ")";
}
}
Javaこのユーザーを「都道府県ごと」にグループにしてみます。
import java.util.*;
import java.util.stream.Collectors;
public class GroupingByPref {
public static void main(String[] args) {
List<User> users = List.of(
new User("Alice", "東京"),
new User("Bob", "大阪"),
new User("Carol", "東京"),
new User("Dan", "福岡")
);
Map<String, List<User>> byPref =
users.stream()
.collect(Collectors.groupingBy(u -> u.prefecture));
byPref.forEach((pref, list) -> {
System.out.println(pref + " : " + list);
});
}
}
Java出力イメージは例えばこうなります。
東京 : [Alice(東京), Carol(東京)]
大阪 : [Bob(大阪)]
福岡 : [Dan(福岡)]
「キーが都道府県」「値がユーザー一覧」の Map<String, List<User>> ができています。
「グループ分けされた Map を使って、都道府県ごとの統計を出す」など、次の処理に繋げやすくなります。
groupingBy の第 2 引数で「グループごとの集計」をする
ここからが groupingBy の本領発揮ポイントなので、少し深掘りします。
ただの List ではなく、「件数」や「合計」が欲しい場合
さっきまでは、値の部分が List<T> でした。
Map<K, List<T>>
だけど、実際には、
「年齢ごとの人数が知りたい」(件数)
「科目ごとの合計点が欲しい」(合計)
など、「リストじゃなくて集計結果が欲しい」ことも多いはずです。
このときに使うのが、第二引数の Collector です。
シグネチャはこうなります。
static <T, K, A, D> Collector<T, ?, Map<K, D>>
groupingBy(Function<? super T, ? extends K> classifier,
Collector<? super T, A, D> downstream)
Java難しい名前は一旦忘れて、
「グループ分けした“その先”で、さらにどうまとめるかを指定できる」
とだけ覚えてください。
例:年齢ごとの人数を数える
さっきの User(age) の例を使って、年齢ごとの人数(int)を出してみます。
import java.util.*;
import java.util.stream.Collectors;
public class GroupingByCounting {
public static void main(String[] args) {
List<User> users = List.of(
new User("Alice", 20),
new User("Bob", 20),
new User("Carol", 30),
new User("Dan", 30),
new User("Eve", 40)
);
Map<Integer, Long> countByAge =
users.stream()
.collect(Collectors.groupingBy(
u -> u.age, // キー:年齢
Collectors.counting() // 各グループの件数を数える
));
System.out.println(countByAge);
// 例: {20=2, 30=2, 40=1}
}
}
JavaCollectors.counting() は、「T を数える Collector」です。
それを groupingBy の第 2 引数に渡すことで、
年齢ごとの「人数(Long)」を値に持つ Map<Integer, Long> ができています。
つまり、
20 → 2人
30 → 2人
40 → 1人
という情報が、一発で取れるわけです。
例:科目ごとの合計点を出す
点数クラスを用意します。
class Score {
String subject;
int value;
Score(String subject, int value) {
this.subject = subject;
this.value = value;
}
}
Java科目ごとの合計点を出したい場合:
import java.util.*;
import java.util.stream.Collectors;
public class GroupingBySumming {
public static void main(String[] args) {
List<Score> scores = List.of(
new Score("Math", 80),
new Score("Math", 90),
new Score("English", 70),
new Score("English", 85)
);
Map<String, Integer> totalBySubject =
scores.stream()
.collect(Collectors.groupingBy(
s -> s.subject, // キー:科目
Collectors.summingInt(s -> s.value) // 各科目の合計点
));
System.out.println(totalBySubject);
// 例: {Math=170, English=155}
}
}
Javaここで使っている Collectors.summingInt は、
「T から int を取り出して合計する Collector」
です。
それを groupingBy の第 2 引数に渡すことで、
科目ごとの合計点を持った Map<String, Integer> が手に入ります。
これが groupingBy の「グループごとの集計」ができる強さです。
partitioningBy との違いをざっくり整理する
groupingBy は「任意のキー」、partitioningBy は「true / false の 2 グループ」
似た名前で partitioningBy がありますが、
groupingBy はキーが何でもいい(Integer, String, Enum, etc.)partitioningBy はキーが boolean 固定(true / false の 2 グループ)
という違いがあります。
例えば、「合格 / 不合格で分けたい」なら partitioningBy でも書けますし、groupingBy で pass / fail みたいな Enum をキーにしても書けます。
初心者のうちは、
「2 分割(true / false)なら partitioningBy もある」
「それ以外(部署ごと、年齢ごと、科目ごと…)は groupingBy」
くらいの整理で十分です。
groupingBy を使うときに意識しておきたいこと
「本当にグループ分けしたいのか?」を自分に確認する
groupingBy は強力ですが、何でもかんでも使うとコードが読みにくくなります。
あなたが今やろうとしているのは、
「ただのフィルタ(絞り込み)か?」
「変換(map)か?」
「それとも、“グループごとの結果”が欲しいのか?」
この中で、「グループごとの結果」が欲しいときにだけ groupingBy を使うと、意図がはっきりします。
例えば、「20 歳以上のユーザーを抽出したい」だけなら filter で十分です。
「年齢ごとに人数を出したい」なら groupingBy の出番です。
for 文で書くとどうなるか、を一度イメージしてみる
もし groupingBy を使うか迷ったら、「for 文で書いたらどうなるか」を頭でシミュレーションしてみてください。
年齢ごとにユーザーをまとめる for 文は、こうなります。
Map<Integer, List<User>> map = new HashMap<>();
for (User u : users) {
List<User> list = map.get(u.age);
if (list == null) {
list = new ArrayList<>();
map.put(u.age, list);
}
list.add(u);
}
Javaこれを一行で書くのが、
Map<Integer, List<User>> byAge =
users.stream()
.collect(Collectors.groupingBy(u -> u.age));
Javaです。
「この for 文パターンが出てきたら groupingBy に置き換えられるな」と分かると、一気に楽になります。
まとめ:groupingBy を自分の中でどう位置づけるか
groupingBy を初心者向けに一言でまとめると、
「Stream の要素を“あるキーごと”にグループ分けして、Map<キー, グループ> を作るための Collector」
です。
特に大事なポイントはこうです。
- グループのキーは Function(
T -> K)で指定する(u -> u.ageなど) - 基本形は
Map<K, List<T>>だが、第 2 引数に別の Collector を渡すとMap<K, 何か>にできる Collectors.counting,summingIntなどと組み合わせて、「グループごとの人数・合計・平均」などの集計ができる- for 文で書くと「Map から get → null チェック → new List → put → add」というお決まりパターンを、一行で置き換えられる
