CodeGym /コース /JAVA 25 SELF /Comparable インターフェイス: 実装と compareTo

Comparable インターフェイス: 実装と compareTo

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

1. オブジェクト比較の問題

おさらい: 参照の比較とオブジェクトの比較

ご存知のとおり、Java でオブジェクトに対して演算子 == を使うと参照を比較します――つまり、同じメモリアドレスを指しているかどうかです。フィールドが同じでも、new で別々に生成した 2 つのオブジェクトは == では異なるものと見なされます。

Person p1 = new Person("Sasha", 20);
Person p2 = new Person("Sasha", 20);

System.out.println(p1 == p2); // false — これはメモリ上で異なるオブジェクト!

中身が等しいかどうかを知りたい場合は equals()hashCode() を使います……これは既にご存知ですね。では、誰が「年上」か、「年下」か、「辞書順でどちらが先か」を知りたいときはどうすればよいでしょうか。たとえば、ユーザーのリストを年齢や名前でソートしたい場合です。

オブジェクトのソートと検索の必要性

例えば、ユーザーのリストがあり、年齢で並べ替えたいとします:

List<Person> people = new ArrayList<>();
people.add(new Person("Vasya", 25));
people.add(new Person("Petya", 20));
people.add(new Person("Katya", 30));

// どうやってソートする?
Collections.sort(people); // おっと!Java は Person の比較方法を知りません!

コンパイラはすぐに文句を言います。クラス Person がインターフェイス Comparable を実装していないからです。Java は私たちの考えを読めませんし、Person にとっての「大きい」「小さい」が何を意味するのか知りません。これを教えるには、比較の規則を明示的に記述する必要があります。

2. Comparable インターフェイス

インターフェイスの宣言

インターフェイス Comparable は、Java に「このクラスは比較可能で、その方法はこれです」と伝える標準的な手段です。

public interface Comparable<T> {
    int compareTo(T o);
}

おなじみの a.compareTo(b) は次を返します:

  • 負の数 — つまり、ab より「小さい」。
  • 0 の場合 — オブジェクトは等しいと見なされます。
  • 正の数ab より「大きい」。

例: Person クラスの compareTo 実装

年齢で比較できるクラス Person を作ってみましょう:

public class Person implements Comparable<Person> {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // ゲッター(後の例で使用)
    public String getName() { return name; }
    public int getAge() { return age; }

    // compareTo メソッドの実装
    @Override
    public int compareTo(Person other) {
        // 年齢でソート(昇順)
        return Integer.compare(this.age, other.age);
        // 代替案: return this.age - other.age;
    }
}

重要なポイント: 降順にしたい場合は引数を入れ替えるだけです: Integer.compare(other.age, this.age)

たとえ話。compareTo は競技の審判のようなものです。誰が先で、誰が後で、誰が同着かを明確に判断しなければなりません。すべての審判(メソッド compareTo)がばらばらの基準で判定したら――カオスになります!

3. Comparable の利用

Comparable によるコレクションのソート

クラスが Comparable を実装したので、ソートは「そのまま」動きます:

List<Person> people = new ArrayList<>();
people.add(new Person("Vasya", 25));
people.add(new Person("Petya", 20));
people.add(new Person("Katya", 30));

Collections.sort(people); // compareTo を使います!

for (Person p : people) {
    System.out.println(p.getName() + " (" + p.getAge() + ")");
}
// Petya (20)
// Vasya (25)
// Katya (30)

同様に、リストのメソッド sort でも動作します:

people.sort(null); // null を渡すと compareTo が使われる

名前でのソート

名前でソートしたい場合は、実装を次のように変えます:

@Override
public int compareTo(Person other) {
    return this.name.compareTo(other.name);
}

複数フィールドでのソート

まずあるフィールドで比べ、同値なら別のフィールドで比べる、ということもあります:

@Override
public int compareTo(Person other) {
    int cmp = Integer.compare(this.age, other.age);
    if (cmp != 0) return cmp;
    return this.name.compareTo(other.name);
}

4. Comparable 実装のベストプラクティス

Comparable の契約を守る

  • もし a.compareTo(b) == 0 なら、b.compareTo(a) も必ず 0
  • もし a.compareTo(b)< 0 なら、b.compareTo(a)> 0(逆も同様)。
  • もし a.compareTo(b) == 0 のとき、できれば a.equals(b)true であること(ただし厳密な必須ではない)。

なぜ重要か?
コレクション(例えば TreeSetTreeMap)やソート処理は、この契約が破られると予測不能に振る舞うことがあります。例えば、本来重複を許さないコレクションに「二重登録」が発生することがあります。

equals と hashCode も忘れずに

compareTo を実装するなら、equalshashCode の実装が適切かどうかも考えましょう。特に、HashSetMap といったコレクションで使用する場合です。

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Person)) return false;
    Person other = (Person) o;
    return age == other.age && Objects.equals(name, other.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

compareTo で null になり得るフィールドを無検査で使わない!

フィールドが null になり得る場合は、安全な比較を使いましょう:

@Override
public int compareTo(Person other) {
    return Objects.compare(this.name, other.name, Comparator.nullsFirst(String::compareTo));
}

オブジェクトが既に整列済みコレクション内にあるなら、compareTo に関与するフィールドを変更しない

これを行うと、コレクション内――例えば TreeSetTreeMap――でオブジェクトが「見失われる」おそれがあります。

5. 学習用アプリを発展させる: ユーザーのソート

ステップ 1: クラスを定義する

public class Person implements Comparable<Person> {
    private String name;
    private int age;

    // ... コンストラクタ、ゲッター、compareTo、equals、hashCode ...
}

ステップ 2: ユーザーを追加する

List<Person> people = new ArrayList<>();
people.add(new Person("Masha", 23));
people.add(new Person("Grisha", 19));
people.add(new Person("Anya", 25));

ステップ 3: ソートして出力する

Collections.sort(people);

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

結果:

Grisha (19)
Masha (23)
Anya (25)

6. Comparable の動作の流れ

┌────────────────────────────┐
│   あなたのクラス (Person)  │
├────────────────────────────┤
│ implements Comparable      │
│   ↓                        │
│ public int compareTo(T o)  │
│   ↓                        │
│ (this < o) → -1            │
│ (this == o) → 0            │
│ (this > o) → 1             │
└────────────────────────────┘
        │
        ▼
Collections.sort(list)
        │
        ▼
   ソートが動く!

7. 役立つニュアンス

Collections.sort の仕組み

  • リスト内のオブジェクトが Comparable を実装している場合、ソートはそのメソッド compareTo を使用します。
  • 実装していない場合はコンパイルエラーになります。
  • 標準型(IntegerString など)には既に Comparable が実装されています。

複数の比較方法を用意できるか?

  • 1 つのクラスにおける「自然順序」は Comparable で 1 つだけです。
  • 別の順序付けが必要なら Comparator を使いましょう(次の講義)。

例: 文字列の compareTo

String a = "apple";
String b = "banana";
System.out.println(a.compareTo(b)); // 負の数。なぜなら "apple" < "banana" だから

表: compareTo が返すもの

比較 戻り値
this < o
< 0
this == o
0
this > o
> 0

8. Comparable 実装での典型的なミス

誤り1: compareTo の契約違反。
もし a.compareTo(b)0 を返し、b.compareTo(a)0 でないなら、コレクションは奇妙な振る舞いをします。例えば、TreeSet がオブジェクトを別物とみなし、両方を追加してしまうことがあります。

誤り2: 未初期化(null)のフィールドを使う。
比較に使うフィールドが null になり得るのに検査をしないと、NullPointerException を招きます。

誤り3: compareTo と equals の不一致。
compareTo がオブジェクトは等しい(0)と言っているのに、equals が異なる(false)と言うと、コレクション操作でバグになります。

誤り4: 整列済みコレクションに追加した後に compareTo が参照するフィールドを変更する。
これは、すでにアルファベット順の列に並んでいるのに途中で名字を変えるようなものです。コレクションがあなたのオブジェクトを「見失う」可能性があります。

誤り5: -1、0、1 だけを返すと思い込む。
メソッド compareTo は、任意の負の数または正の数を返して構いません。必ずしも厳格に -11 だけである必要はありません。ただし、単純化のために -1/0/1 を用いることはよくあります。

1
タスク
JAVA 25 SELF, レベル 29, レッスン 2
ロック未解除
複数基準による書籍カタログのソート 📖
複数基準による書籍カタログのソート 📖
1
タスク
JAVA 25 SELF, レベル 29, レッスン 2
ロック未解除
都市の一意性と順位付けの管理 🏙️
都市の一意性と順位付けの管理 🏙️
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION