CodeGym /課程 /JAVA 25 SELF /Comparable 介面:實作、compareTo

Comparable 介面:實作、compareTo

JAVA 25 SELF
等級 29 , 課堂 2
開放

1. 物件比較的問題

提醒:參考相等與物件比較

你應該已經知道,在 Java 中,運算子 == 用在物件時,比較的是參考(reference)——也就是它們是否位於記憶體中的同一個位址。兩個欄位相同、但透過 new 建立的物件,對 == 來說仍是不同。

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) 會回傳:

  • 負數 —— 表示 a「小於」b
  • 若為 0 —— 物件視為相等。
  • 正數 —— a「大於」b

範例:為 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;
    }

    // Getter(供後續範例使用)
    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 的 Best practices

遵守 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;

    // ... 建構子、getter、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 的實作。

可否提供多種比較方式?

  • 在同一個類別中——只能透過 Comparable 定義一種「自然順序」。
  • 替代的排序方式請使用 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),會在使用集合時引發各種 bug。

錯誤 4:在物件加入排序後的集合之後,仍修改參與 compareTo 的欄位。
這就像你按字母順序排隊時臨時改了姓氏;集合可能會「弄丟」你的物件。

錯誤 5:僅回傳 -1、0 或 1。
compareTo 可以回傳任意負數或正數,不必嚴格是 -11。但為了簡單,很多實作會使用 -1/0/1。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION