排序是程式設計基礎操作。Java 提供兩大核心介面實現物件比較:Comparable (內部比較、自然排序) 與 Comparator (外部比較、定製排序)。本文深度解析兩者區別、實現規範、常見陷阱與實戰選型指南。
public interface Comparable<T> {
int compareTo(T o); // 返回: 負數(小於)、0(等於)、正數(大於)
}
sgn(x.compareTo(y)) == -sgn(y.compareTo(x))(x.compareTo(y) > 0 && y.compareTo(z) > 0) ⇒ x.compareTo(z) > 0(x.compareTo(y) == 0) == x.equals(y)
SortedSet/SortedMap 協定BigDecimal 的 compareTo 與 equals 不一致 (1.0 vs 1.00)null 參數拋 NullPointerException (非規定但慣例)public class Person implements Comparable<Person> {
private String name;
private int age;
private char sex;
// 構造器、getter/setter、toString 省略
@Override
public int compareTo(Person o) {
// 基本類型直接減法 (注意溢出風險,int 安全)
return this.age - o.age;
// 或包裝類 compareTo: Integer.compare(this.age, o.age)
// 字串比較: this.name.compareTo(o.name)
}
}
// TreeSet 自動按自然排序去重
TreeSet<Person> set = new TreeSet<>();
set.add(new Person("李四", 34, '男'));
set.add(new Person("張三", 23, '女'));
set.add(new Person("王五", 13, '男'));
// 遍歷輸出: 王五(13) → 張三(23) → 李四(34)
// Arrays/Collections 排序
List<Person> list = Arrays.asList(p1, p2, p3);
Collections.sort(list); // 依賴 compareTo
@FunctionalInterface
public interface Comparator<T> {
int compare(T o1, T o2);
// JDK 8 默認方法:鏈式組合
default Comparator<T> thenComparing(Comparator<? super T> other) { ... }
default Comparator<T> thenComparing(Function<? super T, ? extends U> keyExtractor) { ... }
static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor) { ... }
static <T> Comparator<T> naturalOrder() { ... }
static <T> Comparator<T> reverseOrder() { ... }
}
public class PersonComparator implements Comparator<Person> {
@Override
public int compare(Person o1, Person o2) {
// 1. 年齡升序
int ageDiff = Integer.compare(o1.getAge(), o2.getAge());
if (ageDiff != 0) return ageDiff;
// 2. 姓名字典序
int nameDiff = o1.getName().compareTo(o2.getName());
if (nameDiff != 0) return nameDiff;
// 3. 性別字符比較
return Character.compare(o1.getSex(), o2.getSex());
}
}
// JDK 8+ 鏈式寫法 (更優雅、可讀性強)
Comparator<Person> comparator = Comparator
.comparing(Person::getAge)
.thenComparing(Person::getName)
.thenComparing(Person::getSex);
// TreeSet 指定比較器
TreeSet<Person> set = new TreeSet<>(new PersonComparator());
// 或匿名內部類
TreeSet<Person> set2 = new TreeSet<>((o1, o2) -> o1.getAge() - o2.getAge());
// List 排序 (不修改原對象,靈活多變)
list.sort(comparator);
Collections.sort(list, comparator);
Arrays.sort(array, comparator);
| 維度 | Comparable | Comparator |
|---|---|---|
| 定義位置 | 類內部 (實現介面) | 類外部 (獨立類/匿名類/Lambda) |
| 比較邏輯 | 固定、唯一 (自然排序) | 可變、多種 (定製策略) |
| 修改權限 | 需修改源碼 | 無需修改源碼 (第三方類也可排序) |
| 適用場景 | 單一默認排序規則 | 多種排序需求、無法修改類 |
| TreeSet/TreeMap 構造 | 無參構造器 | 需傳 Comparator 構造器 |
| Collections.sort() | 單參版本 | 雙參版本 |
| 方法簽名 | compareTo(T o) | compare(T o1, T o2) |
| 函數式介面 | 否 | 是 (JDK 8+) |
// 推薦使用 JDK 7+ 比較器工具
Integer.compare(a, b);
Long.compare(a, b);
Double.compare(a, b); // 處理 NaN 語義
Float.compare(a, b);
// 而非 a - b (可能溢出: Integer.MAX_VALUE - (-1) = 負數 導致邏輯錯誤)
// JDK 8+ 空值友好比較器
Comparator<String> nullsFirst = Comparator.nullsFirst(Comparator.naturalOrder());
Comparator<String> nullsLast = Comparator.nullsLast(Comparator.naturalOrder());
// 自定義: 空值視為最小
Comparator<Person> safe = Comparator
.comparing(Person::getName, Comparator.nullsFirst(String::compareTo))
.thenComparing(Person::getAge);
Comparator<Person> desc = Comparator.comparing(Person::getAge).reversed();
// 或
Collections.sort(list, Collections.reverseOrder(comparator));
// 錯誤示範: 違反傳遞性
Comparator<Person> bad = (o1, o2) -> {
if (o1.getAge() == o2.getAge()) return 0;
return o1.getName().compareTo(o2.getName()); // 年齡不同卻按名字排,破壞傳遞性
};
// TreeSet 使用壞比較器會導致元素丟失、查找失效、死循環
Comparable (如 String、Integer、Date、實體類主鍵)Comparator (如按姓名/年齡/薪資排序員工)Comparable 或構造時傳 Comparatorimport java.util.*;
public class SortDemo {
static class Person {
String name; int age; char sex;
Person(String n, int a, char s) { name=n; age=a; sex=s; }
public String getName() { return name; }
public int getAge() { return age; }
public char getSex() { return sex; }
public String toString() { return name+"("+age+","+sex+")"; }
}
public static void main(String[] args) {
List<Person> list = Arrays.asList(
new Person("李四", 34, '男'),
new Person("張三", 23, '女'),
new Person("王五", 23, '男'), // 同年齡測試次序
new Person("趙六", 13, '女')
);
System.out.println("原始: " + list);
// 1. Comparable 自然排序 (需 Person 實現 Comparable)
// Collections.sort(list);
// 2. Comparator 定製: 年齡升序 → 姓名升序
list.sort(Comparator.comparing(Person::getAge)
.thenComparing(Person::getName));
System.out.println("年齡→姓名: " + list);
// 3. 年齡降序 → 姓名升序
list.sort(Comparator.comparing(Person::getAge).reversed()
.thenComparing(Person::getName));
System.out.println("年齡降→姓名: " + list);
// 4. TreeSet 自動去重排序
TreeSet<Person> set = new TreeSet<>(Comparator
.comparing(Person::getAge)
.thenComparing(Person::getName)
.thenComparing(Person::getSex));
set.addAll(list);
System.out.println("TreeSet: " + set);
}
}
核心心法:
Comparable定義「這個物件本質上怎麼排」,Comparator定義「在這個場景下怎麼排」。前者是物件的內在屬性,後者是外部策略。JDK 8+Comparator鏈式 API 讓複雜排序邏輯變得清晰、可組合、易測試,是現代 Java 排序的最佳實踐。
掌握比較器契約 (反對稱、傳遞、與 equals 一致) 並善用 JDK 8+ 工具方法,能讓排序代碼既正確又優雅。