CodeGym /コース /JAVA 25 SELF /Comparator インターフェイス: 作成と使用

Comparator インターフェイス: 作成と使用

JAVA 25 SELF
レベル 29 , レッスン 3
使用可能

1. はじめに

現実のアプリでは、オブジェクトを比較する方法が1つだけで足りることはまれです。ユーザーのリストがあるとして、名前で並べたいこともあれば、年齢、あるいは姓の長さで並べたいこともあります。あるいは、そのクラスは自分の所有物ではなく(外部ライブラリなど)、そこに compareTo を追加できない場合もあります。そんなときのために Java にはインターフェイス Comparator があります。

Comparable では足りないとき

  • クラスを変更できない(外部ライブラリなど)。
  • 複数の並べ替え方法が必要(異なるフィールドごと)。
  • 比較ロジックをクラス本体から切り離したい(プログラムの場所によって別の並べ方をしたい)。

たとえ話
Comparable がオブジェクトに組み込まれた「自然順序」だとすれば、Comparator は外部の審判です。今日は名前、明日は年齢、あさっては名前の長さ、といった任意の基準で評価できます。

2. Comparator インターフェイス: 構文と契約

インターフェイスの宣言

public interface Comparator<T> {
    int compare(T o1, T o2);
}

メソッド compare の戻り値の意味:

  • 第1引数が第2引数より「小さい」場合は負の数。
  • 等しい場合は 0。
  • 第1引数が第2引数より「大きい」場合は正の数。

契約は Comparable と同様ですが、ここでは compareTo のように「自分」と「相手」を比べるのではなく、2つのオブジェクトを受け取って比較します。

例: 姓でソートするコンパレータ

たとえば、クラス Person があるとします:

public class Person {
    private String firstName;
    private String lastName;
    private int age;

    // コンストラクタとゲッター
    public Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
}

姓で並べ替えるコンパレータを作成します:

import java.util.Comparator;

public class LastNameComparator implements Comparator<Person> {
    @Override
    public int compare(Person a, Person b) {
        return a.getLastName().compareTo(b.getLastName());
    }
}

注意: 文字列(String)の compareTo は辞書順で比較します。

3. Comparator の使い方: コレクションのソート

コンパレータを使ったソート

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person("Anna", "Kostetskaya", 25));
        people.add(new Person("Boris", "Novak", 20));
        people.add(new Person("Viktoriya", "Bell", 22));

        // 姓でソート
        Collections.sort(people, new LastNameComparator());

        for (Person p : people) {
            System.out.println(p.getLastName() + " " + p.getFirstName());
        }
    }
}

結果:

Novak Boris
Kostetskaya Anna
Bell Viktoriya

コンパレータで年齢ソート

クラスがすでに名前で Comparable を実装していても、年齢用の別コンパレータを作成できます:

public class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person a, Person b) {
        return Integer.compare(a.getAge(), b.getAge());
    }
}

使い方は同様です:

Collections.sort(people, new AgeComparator());

結果:

Boris Novak (20)
Viktoriya Bell (22)
Anna Kostetskaya (25)

例: その場でコンパレータを切り替える

Collections.sort(people, new LastNameComparator()); // 姓で
Collections.sort(people, new AgeComparator());      // 年齢で

4. 匿名クラスとラムダ式

コンパレータは、専用クラスを定義せずその場で作成できます。

匿名クラス

Collections.sort(people, new Comparator<Person>() {
    @Override
    public int compare(Person a, Person b) {
        return a.getFirstName().compareTo(b.getFirstName());
    }
});

ラムダ式

Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));

さらに短く、リストのメソッド List.sort でも書けます:

people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
  • 匿名クラスは古い方法で冗長。
  • ラムダは現代的で簡潔。

5. 例: さまざまな基準でのソート

姓の長さでソート

Comparator<Person> byLastNameLength = (a, b) ->
        Integer.compare(a.getLastName().length(), b.getLastName().length());
people.sort(byLastNameLength);

年齢でソートし、同値なら名で(多段)

Comparator<Person> byAgeThenName = (a, b) -> {
    int cmp = Integer.compare(a.getAge(), b.getAge());
    if (cmp != 0) return cmp;
    return a.getFirstName().compareTo(b.getFirstName());
};
people.sort(byAgeThenName);

検索でのコンパレータ利用(例)

コンパレータはソートだけでなく、ソート済みコレクションでの検索にも使えます:

// people は年齢でソートされている必要がある!
Person key = new Person("?", "?", 22);
int idx = Collections.binarySearch(people, key, new AgeComparator());
if (idx >= 0) {
    System.out.println("年齢が22の人が見つかりました: " + people.get(idx));
}

6. Comparator のベストプラクティスと注意点

契約を破らない

  • compare(a, b)0 を返すなら、compare(b, a)0 を返すべき。
  • compare(a, b)0 より大きいなら、compare(b, a)0 より小さいべき。
  • 想定される null 値を考慮する(下記参照)。

equals と hashCode を忘れない

コンパレータは「独自の基準」で比較しますが、TreeSet のような構造や TreeMap のキー検索では、コンパレータの比較ロジックが equals と整合していることが重要です。そうでないと予期しない結果になります。たとえば、コンパレータでは等しいと判定されるのに、equals では等しくない、といった状況です。

null を考慮したソート

フィールドが null になり得る場合は、次のような「用意済み」ヘルパーを使いましょう:

Comparator<Person> byLastNameNullSafe = Comparator.comparing(
    Person::getLastName,
    Comparator.nullsLast(String::compareTo)
);
people.sort(byLastNameNullSafe);

7. 便利なテクニック

表: Comparable と Comparator の比較

Comparable Comparator
どこに実装する? クラス本体 別クラス/ラムダ
メソッド
int compareTo(T o)
int compare(T o1, T o2)
バリエーション数 「自然順序」は1つだけ 必要なだけ作れる
使用方法
Collections.sort(list)
Collections.sort(list, comp)
他人のクラスにも使える? 不可

例: 降順ソート

順序を自前で反転する方法:

Comparator<Person> byAgeDesc = (a, b) -> Integer.compare(b.getAge(), a.getAge());
people.sort(byAgeDesc);

reversed() を使う方法:

Comparator<Person> byAge = Comparator.comparingInt(Person::getAge);
people.sort(byAge.reversed());

8. Comparator での典型的な誤り

誤り1: 比較契約の破り。 compare(a, b)compare(b, a) の符号が反対であるべきことを忘れたり、任意の値(たとえば単なる差分 — a.getAge() - b.getAge()。これはオーバーフローし得ます)を返すと、結果は不定になります。減算ではなく Integer.compare を使いましょう。その方が安全です。

誤り2: null を無視する。 比較に使うフィールドが null になり得るなら、必ずこのケースを処理しましょう(Comparator.nullsFirst/Comparator.nullsLast など)。そうしないと、思わぬタイミングで NullPointerException を招きます。

誤り3: 不安定な比較基準。 同じオブジェクトに対して異なる値を返し得るコンパレータ(乱数や大きく変化するフィールドに依存するなど)だと、ソートは混乱します。

誤り4: equals との不整合。 compare(a, b) == 0 なのに a.equals(b)false の場合、TreeSetTreeMap のようなコレクションは期待どおりに動かないことがあります。コンパレータによる等価性と equals は可能な限り一致させましょう。

誤り5: 外部クラスをコンパレータなしでソートしようとする。 Comparable を持たない「他人の」クラスを、Comparator も渡さずにソートしようとするとコンパイルエラーになります。明示的にコンパレータを渡しましょう。

1
タスク
JAVA 25 SELF, レベル 29, レッスン 3
ロック未解除
イベント参加者リストを年齢順にソートする 🧑‍🎓
イベント参加者リストを年齢順にソートする 🧑‍🎓
1
タスク
JAVA 25 SELF, レベル 29, レッスン 3
ロック未解除
バッジ用の名前の長さによる参加者の並べ替え 🏷️
バッジ用の名前の長さによる参加者の並べ替え 🏷️
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION