PartitioningBy / GroupingBy(Collectors) — 集計・グループ化
Stream の終端操作 collect における定番が partitioningBy(2分割)と groupingBy(多分割)。「条件で振り分ける」「キーで束ねる」を1行で書け、さらに件数や合計などの集計も同時に行えます。初心者でも迷わない使い分けと、よく使う下流コレクタ(downstream)を例付きで整理します。
基本の考え方
- partitioningBy: 真偽(Predicate)で「true/false」2グループに分ける。結果は Map<Boolean, List<T>>。
- groupingBy: キー(Function)で複数グループに分ける。結果は Map<K, List<T>>。
- downstream(下流): デフォルトでは「各グループごとに List<T>」だが、
countingやsummingIntなどを渡すと「件数」「合計」「平均」「別型」へ直接集約できる。
partitioningBy(条件で2分割)
基本(偶数・奇数で分ける)
import java.util.*;
import java.util.stream.*;
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
Map<Boolean, List<Integer>> evenOdd = nums.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
System.out.println(evenOdd.get(true)); // 偶数: [2, 4, 6]
System.out.println(evenOdd.get(false)); // 奇数: [1, 3, 5]
Java下流で件数・平均などを直接出す
// 各グループの要素数
Map<Boolean, Long> countByParity = nums.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0, Collectors.counting()));
// 各グループの平均(double)
Map<Boolean, Double> avgByParity = nums.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0,
Collectors.averagingInt(n -> n)));
Java- コツ: partitioningBy は常にキーが true/false の2つ。グループが空でもキーは存在します。
groupingBy(キーで多分割)
基本(長さで分ける)
import java.util.*;
import java.util.stream.*;
List<String> words = List.of("apple", "banana", "pear", "plum", "kiwi");
Map<Integer, List<String>> byLen = words.stream()
.collect(Collectors.groupingBy(String::length));
System.out.println(byLen.get(5)); // ["apple"]
System.out.println(byLen.get(6)); // ["banana"]
Java下流を使って「件数」「合計」「最大」へ
record User(String name, int age) {}
List<User> users = List.of(
new User("Tanaka", 30), new User("Sato", 25), new User("Kato", 25), new User("Ito", 40)
);
// 年齢でグループ化 → 件数
Map<Integer, Long> countByAge = users.stream()
.collect(Collectors.groupingBy(User::age, Collectors.counting()));
// 年齢でグループ化 → 名前のリスト(mapping)
Map<Integer, List<String>> namesByAge = users.stream()
.collect(Collectors.groupingBy(User::age,
Collectors.mapping(User::name, Collectors.toList())));
// 名前の頭文字でグループ化 → 最大年齢(maxBy)
Map<Character, Integer> maxAgeByInitial = users.stream()
.collect(Collectors.groupingBy(u -> u.name().charAt(0),
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingInt(User::age)),
opt -> opt.map(User::age).orElse(0)
)));
Java- コツ:
mappingは「値を別型に変換してから集約」する定番。collectingAndThenは「最終加工」を挟めます。
下流コレクタの便利レシピ
よく使うもの
- counting: 件数
- summingInt/Long/Double: 合計
- averagingInt/Long/Double: 平均
- maxBy/minBy: 最大・最小(Optional 返し)
- mapping: 値を取り出して別の Collector へ
- joining: 文字列結合(区切り/前後/間も指定可)
- collectingAndThen: 最終加工(例: Optional の中身を抽出)
例: 部門別の合計給与とメンバー名一覧
record Emp(String dept, String name, int salary) {}
List<Emp> emps = List.of(
new Emp("Sales", "Tanaka", 300),
new Emp("Sales", "Sato", 250),
new Emp("Dev", "Ito", 500)
);
// 合計給与
Map<String, Integer> sumSalary = emps.stream()
.collect(Collectors.groupingBy(Emp::dept, Collectors.summingInt(Emp::salary)));
// メンバー名一覧
Map<String, List<String>> namesByDept = emps.stream()
.collect(Collectors.groupingBy(Emp::dept,
Collectors.mapping(Emp::name, Collectors.toList())));
Java例題で身につける
例題1: 合格点で partitioning(合格/不合格)+人数
List<Integer> scores = List.of(72, 88, 90, 65, 99, 80);
Map<Boolean, Long> passFailCount = scores.stream()
.collect(Collectors.partitioningBy(s -> s >= 80, Collectors.counting()));
System.out.println(passFailCount); // {false=2, true=4}
Java例題2: 年齢で grouping(件数と平均を同時に)
record User(String name, int age) {}
List<User> users = List.of(
new User("Tanaka", 30), new User("Sato", 25), new User("Kato", 25), new User("Ito", 40)
);
// 件数
Map<Integer, Long> countByAge = users.stream()
.collect(Collectors.groupingBy(User::age, Collectors.counting()));
// 平均(年齢キーの平均はその年齢と同じになるため、部署など別キーでの平均が現実向き)
Map<Character, Double> avgAgeByInitial = users.stream()
.collect(Collectors.groupingBy(u -> u.name().charAt(0),
Collectors.averagingInt(User::age)));
Java例題3: 複合キーで grouping(多段ネスト)
record Tx(String user, String category, int amount) {}
List<Tx> txs = List.of(
new Tx("Tanaka", "Food", 1200),
new Tx("Tanaka", "Book", 800),
new Tx("Sato", "Food", 1500),
new Tx("Sato", "Food", 700)
);
Map<String, Map<String, Integer>> sumByUserCategory = txs.stream()
.collect(Collectors.groupingBy(Tx::user,
Collectors.groupingBy(Tx::category, Collectors.summingInt(Tx::amount))));
System.out.println(sumByUserCategory);
// {Tanaka={Food=1200, Book=800}, Sato={Food=2200}}
Javaテンプレート集と実務のコツ
- 2分割(件数/合計/平均)
Map<Boolean, Long> counts = stream.collect(
Collectors.partitioningBy(pred, Collectors.counting()));
Map<Boolean, Integer> sums = stream.collect(
Collectors.partitioningBy(pred, Collectors.summingInt(toInt)));
Java- 多分割(件数/合計/最大)
Map<K, Long> counts = stream.collect(
Collectors.groupingBy(key, Collectors.counting()));
Map<K, Integer> sums = stream.collect(
Collectors.groupingBy(key, Collectors.summingInt(toInt)));
Map<K, T> maxs = stream.collect(
Collectors.groupingBy(key,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingInt(toIntProp)),
Optional::get)));
Java- 値変換してから集約(mapping)
Map<K, List<U>> result = stream.collect(
Collectors.groupingBy(key, Collectors.mapping(mapper, Collectors.toList())));
Java- 順序保持(LinkedHashMap)や EnumMap 指定
Map<K, V> ordered = stream.collect(
Collectors.groupingBy(key, LinkedHashMap::new, downstream));
Map<MyEnum, V> enumMap = stream.collect(
Collectors.groupingBy(e -> e.type(), () -> new EnumMap<>(MyEnum.class), downstream));
Java- 落とし穴と回避
- 巨大グループの List が重い:
counting/summing/maxByで直接集約してメモリ節約。 - Optional の扱いが煩雑:
collectingAndThen(..., Optional::orElse(default))で最終加工。 - キーが null: groupingBy のキー関数は null を返さない設計にする(NullPointer 問題の予防)。
- 読みやすさ: ネストが深くなったら変数に受ける、関数を事前定義して可読性を保つ。
- 巨大グループの List が重い:
まとめ
- partitioningBy は「条件で2分割」、groupingBy は「キーで多分割」。
- 下流コレクタを組み合わせると、「各グループの件数・合計・平均・最大」などが一発で得られる。
- メモリと可読性を意識して、必要な最終形(件数・合計・リスト・Map)に直接集約するのが実務のコツ。
