「名前順で並べたら、年齢が同じときどうする?」という壁

Javaでリストをソートするとき、「名前順に並べたい」という要件は簡単に実装できます。しかし実務や試験では、「名前が同じ場合は年齢順、年齢も同じ場合はIDの昇順」といった複数条件のソートが問われます。

Java Gold 1Z0-826の試験では、Comparatorを連鎖させる書き方が頻出です。thenComparing()の使い方を知らないまま臨むと、正しそうに見えるコードの選択肢に引っかかってしまいます。

この記事では、複数条件ソートの基本から試験でよく問われるひっかけポイントまで、コード例を交えながら順を追って解説します。


Comparatorの基本を30秒でおさらいする

Comparator<T>はオブジェクトの順序を定義するための関数型インターフェースです。compare(T o1, T o2)メソッドが本体で、戻り値の符号によってソート順が決まります。

戻り値意味
負の値o1 を o2 より前に置く
0順序は同じ
正の値o1 を o2 より後に置く

Java 8以降はComparator.comparing()などの静的メソッドが追加され、ラムダ式を使ったシンプルな記述が主流になっています。


単一条件ソートの書き方(おさらい)

まず単一条件の書き方を確認します。以下のサンプルクラスを前提とします。

record Person(String name, int age, int id) {}

名前の昇順でソートする場合は次のように書きます。

List<Person> people = new ArrayList<>(List.of(
    new Person("Yamada", 30, 3),
    new Person("Ito", 25, 1),
    new Person("Yamada", 22, 2)
));

people.sort(Comparator.comparing(Person::name));

people.forEach(p -> System.out.println(p.name() + " " + p.age() + " " + p.id()));
// Ito 25 1
// Yamada 30 3
// Yamada 22 2

名前が同じ”Yamada”が2件ありますが、この段階では順序が不定です。これが複数条件ソートが必要になる理由です。


thenComparingで複数条件を連鎖させる

thenComparing()は、前の条件が同一だった場合に適用される次の比較条件を追加するメソッドです。メソッドチェーンで直感的に記述できます。

Comparator<Person> comp = Comparator
    .comparing(Person::name)          // 第1条件:名前の昇順
    .thenComparingInt(Person::age)    // 第2条件:年齢の昇順
    .thenComparingInt(Person::id);    // 第3条件:IDの昇順

people.sort(comp);

people.forEach(p -> System.out.println(p.name() + " " + p.age() + " " + p.id()));
// Ito 25 1
// Yamada 22 2
// Yamada 30 3

thenComparing()の代わりにthenComparingInt()を使うと、int型をオートボクシングせずに比較できるためパフォーマンス上有利です。同様にthenComparingLong()、thenComparingDouble()も存在します。


reversedで降順に切り替える

昇順・降順を混在させたい場合はreversed()を使います。

Comparator<Person> comp = Comparator
    .comparing(Person::name)               // 名前:昇順
    .thenComparingInt(Person::age).reversed(); // ← reversed()の位置に注意!

⚠️ ひっかけポイント:reversed()はチェーン全体に作用します。上記のコードは「名前の降順、年齢の降順」になり、意図した動作と異なります。

特定の条件だけを降順にしたい場合は、対象のComparatorに対して個別にreversed()を呼びます。

Comparator<Person> comp = Comparator
    .comparing(Person::name)
    .thenComparing(Comparator.comparingInt(Person::age).reversed()); // 年齢のみ降順

people.sort(comp);

people.forEach(p -> System.out.println(p.name() + " " + p.age() + " " + p.id()));
// Ito 25 1
// Yamada 30 3   ← 年齢が高い方が先
// Yamada 22 2

nullを含む場合のソート

実務ではnullを含むリストのソートが問われることもあります。Comparator.nullsFirst()とComparator.nullsLast()を使うと、null要素の位置を明示的に制御できます。

List<String> names = new ArrayList<>(Arrays.asList("Yamada", null, "Ito"));

names.sort(Comparator.nullsFirst(Comparator.naturalOrder()));

System.out.println(names);
// [null, Ito, Yamada]

names.sort(Comparator.nullsLast(Comparator.naturalOrder()));

System.out.println(names);
// [Ito, Yamada, null]

nullを含むリストに何も対策せずComparator.naturalOrder()を使うと、実行時にNullPointerExceptionが発生します。これは試験で「このコードを実行するとどうなるか」という形式で問われる典型的なパターンです。


試験頻出メソッドの比較表

複数条件ソートで使うメソッドをまとめます。

メソッド役割補足
Comparator.comparing(keyExtractor)第1ソートキーを指定参照型を対象とする
Comparator.comparingInt(keyExtractor)int型のソートキーを指定ボクシングなし
.thenComparing(keyExtractor)次のソートキーを追加参照型
.thenComparingInt(keyExtractor)次のソートキーを追加int型特化
.reversed()直前までのComparator全体を逆順部分逆順には使えない
Comparator.naturalOrder()自然順序(Comparable準拠)nullがあるとNPE
Comparator.reverseOrder()自然順序の逆nullがあるとNPE
Comparator.nullsFirst(comp)nullを先頭に配置ラップして使う
Comparator.nullsLast(comp)nullを末尾に配置ラップして使う

よく出るひっかけポイントまとめ

① reversed()はチェーン全体に影響する

thenComparing()の後に.reversed()を呼ぶと、それ以前のすべての条件が逆順になります。部分的に逆順にしたい場合は、Comparator.comparingInt(...).reversed()をthenComparing()の引数に渡す方法を使います。

② thenComparing()は第1条件が等しい場合にのみ評価される

第1条件でソートが確定した場合、thenComparing()以降は呼ばれません。これはパフォーマンスにとって有利ですが、「必ず全条件で比較される」と誤解しているとコードの読み違いにつながります。

③ Comparator.comparing()とComparable.compareTo()は別物

Comparatorは外部から順序を与えるオブジェクトです。Comparableはクラス自身が順序を持つためのインターフェースです。試験では両者の役割の違いが問われることがあります。

④ List.sort()とCollections.sort()の違い

どちらも安定ソート(等しい要素の元の順序を保持)ですが、List.sort()はJava 8で追加されたデフォルトメソッドです。Collections.sort()は内部でList.sort()を呼び出しているため、動作は同じです。試験では「どちらが安定ソートか」という設問が出ることがあります。答えは「両方とも安定ソート」です。


まとめ

複数条件ソートの核心はthenComparing()のチェーンとreversed()の適用範囲の理解です。

  • 複数条件はthenComparing() / thenComparingInt()でつなぐ
  • reversed()はチェーン全体に作用するため、部分的な降順には個別に適用する
  • nullを含む場合はnullsFirst() / nullsLast()でラップする
  • List.sort()もCollections.sort()も安定ソートである

これらを押さえておけば、試験で複数条件ソートに関する問題が出ても落ち着いて対処できます。コードを手で書いて動作を確認しながら覚えることが、確実な得点につながる近道です。