首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Comparator 與 Comparable 深度解析:排序介面選型、實作技巧與避坑指南

     編輯:Java 相關知識

一眼看懂核心差異

特性Comparable (內部比較)Comparator (外部比較)
定義位置類別內部 implements Comparable<T>獨立類別 implements Comparator<T>
方法簽名int compareTo(T o)int compare(T o1, T o2)
排序策略數量唯一「自然排序」無限多種 (依場景注入)
適用場景領域物件有明確預設順序 (如 ID、時間)需多種排序、無法修改原類、第三方類
Collections.sort 叫法sort(list)sort(list, comparator)

Comparable 實作範本 (自然排序)

public class User implements Comparable<User> {
    private Long id;
    private String name;
    private LocalDate birthDate;

    @Override
    public int compareTo(User other) {
        // 1. 防呆
        if (other == null) return 1; // null 排最後
        // 2. 主鍵優先 (穩定、唯一)
        int cmp = Long.compare(this.id, other.id);
        // 3. 若主鍵相同再比次要欄位 (可選)
        // return cmp != 0 ? cmp : this.birthDate.compareTo(other.birthDate);
        return cmp;
    }
}

鐵律:compareTo 必須與 equals 一致 (即 a.compareTo(b)==0 <=> a.equals(b)),否則 TreeSet/TreeMap 會出現「明明不 equal 卻被視為重複」的詭異行為。

Comparator 靈活實作三種風格

1. 獨立類別 (可序列化、可重用)

public class UserByAgeDesc implements Comparator<User>, Serializable {
    @Override
    public int compare(User u1, User u2) {
        if (u1 == u2) return 0;
        if (u1 == null) return 1;
        if (u2 == null) return -1;
        return Integer.compare(u2.getAge(), u1.getAge()); // 降序
    }
}

2. Java 8+ Lambda / 方法參考 (最常用)

// 單欄位
users.sort(Comparator.comparing(User::getAge));                    // 升序
users.sort(Comparator.comparing(User::getAge).reversed());         // 降序

// 多欄位鏈式 (年齡升序 → 姓名降序 → ID 升序)
users.sort(Comparator
    .comparing(User::getAge)
    .thenComparing(Comparator.comparing(User::getName).reversed())
    .thenComparing(User::getId));

// null 安全 (null 排最後)
users.sort(Comparator.nullsLast(Comparator.comparing(User::getName)));

3. 比較器工廠模式 (集中管理排序策略)

public final class UserComparators {
    private UserComparators() {}
    public static final Comparator<User> BY_AGE_ASC = Comparator.comparing(User::getAge);
    public static final Comparator<User> BY_NAME_DESC = Comparator.comparing(User::getName).reversed();
    public static final Comparator<User> BY_REG_DATE = Comparator.comparing(User::getRegisterTime);
}

進階議題

1. 總排序與 equals 一致性驗證

// 單元測試範例
@Test
void compareToConsistentWithEquals() {
    User a = new User(1L, "Alice", 20);
    User b = new User(1L, "Alice", 20); // 同 ID
    User c = new User(2L, "Bob", 25);

    assertEquals(0, a.compareTo(b));
    assertTrue(a.equals(b)); // 必須為 true
    assertNotEquals(0, a.compareTo(c));
}

2. 序列化陷阱

若 Comparator 實例會被放入 TreeSet/TreeMap 並需跨 JVM 傳輸 (如分散式快取),必須實作 Serializable,否則反序列化會噴 NotSerializableException。

3. 原始型別效能優化

// 避免自動裝箱
Comparator<Int> intCmp = (a, b) -> Integer.compare(a, b); // 仍會裝箱
// 真正零裝箱需用專用庫 (如 Trove) 或手寫 primitive 迴圈排序

4. 反轉與鏈式的陷阱

// 錯誤:reversed() 只反轉當前比較器,不影響 thenComparing 後續
Comparator<User> bad = Comparator.comparing(User::getAge).reversed()
                                 .thenComparing(User::getName); // 年齡降序、姓名「升序」✓

// 正確:想全部反轉用比較器包裝
Comparator<User> allDesc = Comparator
    .comparing(User::getAge)
    .thenComparing(User::getName)
    .reversed(); // 年齡降序、姓名降序

實戰決策樹

  1. 類別是你寫的、有明確預設順序 (ID、時間戳) → 實作 Comparable。
  2. 需要第二種排序、或類別不可修改 (第三方/DTO) → 寫 Comparator。
  3. 排序邏輯極簡、僅用一次 → Lambda 內聯。
  4. 排序策略複雜、多處共用、需序列化 → 獨立 Comparator 類別 + 工廠常數。

結語

Comparable 定義「這個物件本質上怎麼排」,Comparator 定義「這個場景下怎麼排」。善用 Java 8 Comparator.comparing/thenComparing/reversed/nullsFirst 鏈式 API,能以極少程式碼表達複雜多層排序,且型別安全、易測試、易維護。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved