概要
一覧データを部署順・金額順でソートする、部署別に人数や合計を集計する、一定の閾値で2グループに分割する。こうした処理は業務システムで頻繁に登場しますが、Comparator の合成や Collectors の組み合わせに慣れるまでは記述が冗長になりがちです。この記事では、Comparator.comparing と thenComparingInt による複合キーソート、groupingBy + counting / summingInt による部門別集計、partitioningBy による二分割、joining による文字列結合、さらに Java 12 で追加された Collectors.teeing による同時集計まで、業務で実際に書く形のコードで解説します。record とメソッド参照を活用して、読みやすさと型安全性を両立させるパターンも示します。
使いどころ
社員一覧を部署昇順 → 給与降順でソートし、管理帳票や画面表示に使う
売上データを商品カテゴリ別に groupingBy で集計し、カテゴリごとの件数・合計金額・平均単価を一覧化する
受注データを金額閾値(例: 100万円以上/未満)で partitioningBy して、承認フローの振り分けに使う
コード例
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class SortAndGrouping {
record Employee(String name, String department, int salary) {
@Override
public String toString() {
return name + "(" + department + ", " + salary + "円)";
}
}
public static void main(String[] args) {
var employees = List.of(
new Employee("田中", "営業", 350000),
new Employee("鈴木", "開発", 420000),
new Employee("佐藤", "営業", 380000),
new Employee("山田", "開発", 450000),
new Employee("伊藤", "人事", 320000),
new Employee("加藤", "開発", 390000)
);
// ① 複合キーソート(部署昇順 → 給与昇順)
var sorted = employees.stream()
.sorted(Comparator.comparing(Employee::department)
.thenComparingInt(Employee::salary))
.toList();
System.out.println("部署→給与順:");
sorted.forEach(e -> System.out.println(" " + e));
// ② 部署別人数(groupingBy + counting)
Map<String, Long> countByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::department, Collectors.counting()));
System.out.println("\n部署別人数: " + countByDept);
// ③ 部署別給与合計(groupingBy + summingInt)
Map<String, Integer> totalByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.summingInt(Employee::salary)));
System.out.println("部署別給与合計: " + totalByDept);
// ④ 部署別平均給与(groupingBy + averagingInt)
Map<String, Double> avgByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.averagingInt(Employee::salary)));
System.out.println("部署別平均給与: " + avgByDept);
// ⑤ 金額閾値で二分割(partitioningBy)
Map<Boolean, List<Employee>> partition = employees.stream()
.collect(Collectors.partitioningBy(
e -> e.salary() >= 400000));
System.out.println("\n40万円以上: " + partition.get(true));
System.out.println("40万円未満: " + partition.get(false));
// ⑥ 名前を区切り文字で結合(joining)
String nameList = employees.stream()
.map(Employee::name)
.collect(Collectors.joining("、"));
System.out.println("\n社員一覧: " + nameList);
// ⑦ 複数集計を1パスで実行(teeing, Java 12+)
record DeptSummary(long count, int total, double average) {}
Map<String, DeptSummary> summaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::department,
Collectors.teeing(
Collectors.counting(),
Collectors.summingInt(Employee::salary),
(count, total) -> new DeptSummary(
count, total,
(double) total / count)
)));
summaryByDept.forEach((dept, s) ->
System.out.printf("%s: %d人, 合計%d円, 平均%.0f円%n",
dept, s.count(), s.total(), s.average()));
}
}Version Coverage
record + メソッド参照で groupingBy のキー指定が簡潔に。Stream.toList() でイミュータブルな結果を取得。teeing も利用可能。
// Java 17: record + メソッド参照で簡潔に
record Employee(String name, String department,
int salary) {}
var sorted = employees.stream()
.sorted(Comparator.comparing(Employee::department)
.thenComparingInt(Employee::salary))
.toList();Library Comparison
注意点
Comparator.comparing のラムダでフィールドの型が曖昧な場合、明示的な型パラメータが必要になる。(Employee e) -> e.department のように引数に型を書くか、メソッド参照 Employee::department を使う
groupingBy のキーに null が含まれると NullPointerException になる。事前に filter(e -> e.department() != null) で除外するか、デフォルト値に置換する
Collectors.toList() が返すリストは可変だが、Stream.toList()(Java 16+)は不変リスト。ソート結果を後から変更する必要があるなら collect(Collectors.toList()) を使う
thenComparing で int フィールドを比較する場合は thenComparingInt を使う。thenComparing(e -> e.salary) だとオートボクシングが発生し、大量データでパフォーマンスに影響する
Collectors.teeing は Java 12 以降で利用可能。Java 8 環境では2回の collect に分けるか、カスタム Collector を書く必要がある
FAQ
comparing はフィールドを抽出するキー関数を受け取って比較します。naturalOrder は Comparable を実装した要素そのものの自然順序で並べます。DTO のフィールド単位でソートするなら comparing を使います。
groupingBy の第2引数に TreeMap::new を渡すと、キーの自然順序でソートされた Map が返ります。Collectors.groupingBy(Employee::department, TreeMap::new, Collectors.toList()) の形です。
2グループへの二分割なら partitioningBy が適しています。戻り値の Map<Boolean, List<T>> で true/false の両グループが必ず返るため、該当なしの場合も空リストが保証されます。3つ以上の分類には groupingBy を使います。