Comparator の作り方(comparing, reversed, thenComparing) — ソートルール
「主キー→副キー→さらに副キー」と、明快に並び替えルールを書けるのが Comparator の魅力。Comparator.comparing、reversed、thenComparing を押さえれば、複雑なソートも読みやすく安全に書けます。初心者向けに、基本から落とし穴、テンプレート、例題まで丁寧に解説します。
基本の考え方
- Comparator とは: 2つの要素の大小を決める「比較器」。
sortやStream.sortedに渡して並びを制御する。 - 主なビルダー:
- comparing(keyExtractor): 「キーを取り出して」その自然順で比べる。
- reversed(): 直前までのルールを丸ごと逆順にする。
- thenComparing(nextKey): 同点時に次のキーで比較する(副キー)。
- 自然順序: 文字列は辞書順、数値は小さい順。独自順序が必要なら
Comparatorを使う。
すぐ使える基本例
1. comparing(主キーだけで並べ替え)
import java.util.*;
record User(String name, int age) {}
List<User> users = new ArrayList<>(List.of(
new User("Tanaka", 30),
new User("Sato", 25),
new User("Kato", 40)
));
// 年齢昇順
users.sort(Comparator.comparingInt(User::age));
System.out.println(users); // [Sato(25), Tanaka(30), Kato(40)]
Java2. reversed(逆順にする)
// 年齢降順(reversed)
users.sort(Comparator.comparingInt(User::age).reversed());
System.out.println(users); // [Kato(40), Tanaka(30), Sato(25)]
Java3. thenComparing(副キーを追加)
// 年齢昇順、同年齢は名前昇順
users.sort(Comparator.comparingInt(User::age)
.thenComparing(User::name));
System.out.println(users); // [Sato(25), Tanaka(30), Kato(40)]
Javaよくある並び替えレシピ
複合キー(主キー降順+副キー昇順)
// 価格降順、同価格なら名前昇順
record Product(String name, int price) {}
List<Product> ps = new ArrayList<>(List.of(
new Product("Tea", 300),
new Product("Coffee", 500),
new Product("Cake", 500)
));
ps.sort(Comparator.comparingInt(Product::price).reversed()
.thenComparing(Product::name));
System.out.println(ps); // [Cake(500), Coffee(500), Tea(300)]
Javaカスタム比較(文字列長→同長は辞書順)
List<String> words = new ArrayList<>(List.of("apple","kiwi","banana","pear","plum"));
words.sort(Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder()));
System.out.println(words); // [kiwi, pear, plum, apple, banana]
Javanull を許容する並び(nullsLast / nullsFirst)
List<String> names = new ArrayList<>(List.of("Tanaka", null, "Sato"));
names.sort(Comparator.nullsLast(Comparator.naturalOrder()));
System.out.println(names); // [Sato, Tanaka, null]
Java大文字小文字を無視した辞書順
List<String> names2 = new ArrayList<>(List.of("tanaka", "Sato", "Kato"));
names2.sort(String.CASE_INSENSITIVE_ORDER);
System.out.println(names2); // [Kato, Sato, tanaka]
Java落とし穴と回避策
- reversed の適用範囲に注意:
reversed()は「それまでの比較器全体」を反転する。途中で部分的に降順にしたいなら「そのキーのところで reverse」を使う(例:Comparator.comparingInt(User::age).reversed().thenComparing(User::name)は、主キーが降順・副キーが昇順になる)。 - キーが null の可能性: そのまま比較すると
NullPointerException。Comparator.nullsFirst/Lastで包むか、事前にObjects.requireNonNull。 - compareTo の引き算でのオーバーフロー: 整数を
a - bで比較するのは危険。comparingIntなどの安全なビルダーを使う。 - equals と順序の一貫性: セットやマップの順序付けに使う比較器は、可能な限り
equalsと整合的な順序を心がける(同値なのに順序が揺れると不具合の原因)。 - 多段チェーンの可読性: 3段以上の
thenComparingが続くなら、変数に受けるか関数を分けて読みやすさを保つ。
例題で身につける
例題1: 期限の早い順、同期限なら優先度高い順
record Job(String id, long deadline, int priority) {}
List<Job> jobs = new ArrayList<>(List.of(
new Job("A", 1000L, 1),
new Job("B", 900L, 3),
new Job("C", 900L, 2)
));
Comparator<Job> byUrgency = Comparator
.comparingLong(Job::deadline) // 期限が早いほど先
.thenComparing(Comparator.comparingInt(Job::priority).reversed()); // 優先度高い順
jobs.sort(byUrgency);
System.out.println(jobs); // [B(900,3), C(900,2), A(1000,1)]
Java例題2: 名前の頭文字昇順、同じ頭文字は長さ降順
List<String> people = new ArrayList<>(List.of("Tanaka","Ito","Iida","Sato","Sakai"));
people.sort(Comparator.comparing((String s) -> s.charAt(0))
.thenComparing(Comparator.comparingInt(String::length).reversed()));
System.out.println(people); // [Iida, Ito, Sakai, Sato, Tanaka]
Java例題3: Map のエントリを「値降順→キー昇順」でソート
Map<String, Integer> score = Map.of("A",10, "C",15, "B",15);
List<Map.Entry<String,Integer>> sorted = new ArrayList<>(score.entrySet());
sorted.sort(Map.Entry.<String,Integer>comparingByValue().reversed()
.thenComparing(Map.Entry.comparingByKey()));
System.out.println(sorted); // [B=15, C=15, A=10]
Javaテンプレート集(そのまま使える形)
- 昇順: 主キーのみ
list.sort(Comparator.comparing(Type::key));
Java- 降順: 主キーのみ
list.sort(Comparator.comparing(Type::key).reversed());
Java- 複合キー: 主キー降順+副キー昇順
list.sort(Comparator.comparing(Type::primary).reversed()
.thenComparing(Type::secondary));
Java- 数値プロパティの比較(安全)
list.sort(Comparator.comparingInt(Type::intKey));
list.sort(Comparator.comparingLong(Type::longKey));
list.sort(Comparator.comparingDouble(Type::doubleKey));
Java- null を許容(末尾に追い出す)
list.sort(Comparator.comparing(Type::key,
Comparator.nullsLast(Comparator.naturalOrder())));
Java- カスタム順序(例:文字列長→辞書順)
list.sort(Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder()));
Java実務でのコツ
- Comparator を変数化: 何度も使う比較ルールは変数で定義し、一覧性と再利用性を高める。
- Stream.sorted と組み合わせ:
stream.sorted(comparator)→collect(toList)で、直感的なデータパイプラインを作れる。 - LinkedHashMap と合わせて順序保持: ソート後に
collect(toMap(..., LinkedHashMap::new))へ落とすと表示順やランキングをそのまま保てる。 - Locale/Collator の考慮: 文字列の比較はロケール依存のケースがある。厳密なソートが必要なら
Collatorを使う。
まとめ
comparingで主キー、thenComparingで副キーを重ね、必要箇所だけreversedで向きを反転すれば、業務ロジック通りのソートが明快に書ける。- null・一貫性・可読性に配慮しつつ、比較器を「宣言的に」組み立てるのが実務の最適解。複合キーの並び替えはテンプレートを使い回して、迷わず正しく。
