Java 逆引き集 | Comparator の作り方(comparing, reversed, thenComparing) — ソートルール

Java Java
スポンサーリンク

Comparator の作り方(comparing, reversed, thenComparing) — ソートルール

「主キー→副キー→さらに副キー」と、明快に並び替えルールを書けるのが Comparator の魅力。Comparator.comparingreversedthenComparing を押さえれば、複雑なソートも読みやすく安全に書けます。初心者向けに、基本から落とし穴、テンプレート、例題まで丁寧に解説します。


基本の考え方

  • Comparator とは: 2つの要素の大小を決める「比較器」。sortStream.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)]
Java

2. reversed(逆順にする)

// 年齢降順(reversed)
users.sort(Comparator.comparingInt(User::age).reversed());
System.out.println(users); // [Kato(40), Tanaka(30), Sato(25)]
Java

3. 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]
Java

null を許容する並び(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 の可能性: そのまま比較すると NullPointerExceptionComparator.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・一貫性・可読性に配慮しつつ、比較器を「宣言的に」組み立てるのが実務の最適解。複合キーの並び替えはテンプレートを使い回して、迷わず正しく。
タイトルとURLをコピーしました