1. 簡介
在實務中,比較物件很少只需要一種方式。想像你有一個使用者清單:有時想依名字排序,有時依年齡,有時則依姓氏長度。或者你手上的類別並非你所撰寫,因此無法為其加入 compareTo。針對這些情境,Java 提供了 Comparator 介面。
何時 Comparable 不夠用
- 類別無法修改(例如來自第三方函式庫)。
- 需要多種排序方式(依不同欄位)。
- 希望將比較邏輯與類別本身分離(例如在程式不同部分採用不同排序方式)。
類比
如果 Comparable 是物件內建的「自然順序」,那麼 Comparator 就是外部的「裁判」,可以依任何標準評分你的物件:今天依名字、明天依年齡、後天依名字長度。
2. Comparator 介面:語法與契約
介面宣告
public interface Comparator<T> {
int compare(T o1, T o2);
}
方法 compare 應回傳:
- 負數,如果第一個物件「小於」第二個。
- 0,如果它們相等。
- 正數,如果第一個「大於」第二個.
契約與 Comparable 相同,只是現在比較的是兩個物件,而不是透過 compareTo 比較「當前」與「另一個」。
範例:依姓氏排序的比較器
假設有一個類別 Person:
public class Person {
private String firstName;
private String lastName;
private int age;
// 建構子與 getter
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. 匿名類別與 Lambda 運算式
比較器可以「即時」建立,無需宣告成獨立類別。
匿名類別
Collections.sort(people, new Comparator<Person>() {
@Override
public int compare(Person a, Person b) {
return a.getFirstName().compareTo(b.getFirstName());
}
});
Lambda 運算式
Collections.sort(people, (a, b) -> a.getFirstName().compareTo(b.getFirstName()));
或更簡潔地使用清單的方法 List.sort:
people.sort((a, b) -> a.getFirstName().compareTo(b.getFirstName()));
- 匿名類別是較舊的作法,顯得冗長。
- Lambda 現代且精簡。
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 | |
|---|---|---|
| 在哪裡實作? | 在類別本身 | 在獨立類別/Lambda |
| 方法 | |
|
| 可有多少種? | 僅一種「自然」 | 不限,多種需求皆可 |
| 用法 | |
|
| 可用於外部類別? | 否 | 是 |
範例:遞減排序
可以手動反轉順序:
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