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 | |
|---|---|---|
| どこに実装する? | クラス本体 | 別クラス/ラムダ |
| メソッド | |
|
| バリエーション数 | 「自然順序」は1つだけ | 必要なだけ作れる |
| 使用方法 | |
|
| 他人のクラスにも使える? | 不可 | 可 |
例: 降順ソート
順序を自前で反転する方法:
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 の場合、TreeSet や TreeMap のようなコレクションは期待どおりに動かないことがあります。コンパレータによる等価性と equals は可能な限り一致させましょう。
誤り5: 外部クラスをコンパレータなしでソートしようとする。 Comparable を持たない「他人の」クラスを、Comparator も渡さずにソートしようとするとコンパイルエラーになります。明示的にコンパレータを渡しましょう。
GO TO FULL VERSION