Java Comparable 和 Comparator 介面詳解

低吟不作語發表於2020-09-25

本文基於 JDK8 分析


Comparable

Comparable 介面位於 java.lang 包下,Comparable 介面下有一個 compareTo 方法,稱為自然比較方法。一個類只要實現了這個介面,意味著該類支援自然排序

所謂自然排序,就是按預設規則組成的排序,例如 1234 就是自然排序,因為 2 就是比 1 大,這是預設規定的。類比到 Comparable,我們在 compareTo 中定義自己需要的預設比較規則,以後如果用到 Collections.sort 和 Arrays.sort 方法排序,或者是作為 SortedSet、SortedMap 等元件的元素,就可以按照我們想要的規則排序了

比較的物件不應該出現 null,因為 null 不屬於任何類的例項。如果出現了 e.compareTo(null) 這種情況,應該丟擲 NullPointerException


Comparable 的用法

Comparable 介面在 JDK8 中的原始碼

// T 是可比較的型別
public interface Comparable<T> {
    public int compareTo(T o);
}

需要比較的類只需實現 Comparable 介面即可,在 compareTo 中定義自己的比較規則

  • 返回 0 表示當前物件與目標物件相等
  • 返回正數表示當前物件比目標物件大
  • 返回負數表示當前物件比目標物件小
public class User implements Comparable<User>{
    private Integer id;
    private Integer age;

    // 構造方法和 set/get 方法省略 ...
	
    // 第一種實現方式
    public int compareTo(User o) {
        // 根據使用者的年齡比較,引數 o 為目標比較物件
        if(this.age > o.getAge()) {
            // 當前物件比目標物件大,則返回 1
            return 1;
        }else if(this.age < o.getAge()) {
            // 當前物件比目標物件小,則返回 -1
            return -1;
        }else{
            // 若是兩個物件相等,則返回 0
            return 0;
        }
    }
    
    // 第二種實現方式
    public int compareTo(User o) {
        return this.age - o.getAge();
    }
}

compareTo 和 equals

強烈建議自然排序和 equals 的順序保持一致(就是兩個物件呼叫 compareTo 方法和呼叫 equals 方法返回的布林值應該一樣)

這個建議在需要同時保持元素有序和唯一的集合中尤其重要。例如 TreeSet,它是一個 Set 集合,通過元素的 hashCode() 和 equals() 來判斷元素是否唯一,同時還會依據 Comparator 或是 Comparable 介面對元素進行排序。假如出現了 equals 和 compareTo 行為不一致,就會出現十分詭異的情況,JDK 官方文件有對該情況的說明:

如果將兩個鍵 a 和 b 新增到沒有使用顯式比較器的有序集合中,使得 (!a.equals(b) && a.compareTo(b) == 0) 成立,那麼第二個 add 操作(新增 b)將返回 false(有序集合的大小沒有增加),因為從有序集合的角度來看,a 和 b 是相等的

明明 equals 已經判斷該元素不重複,但還是拒絕了新增操作,因為 compaTo 認為這兩個元素是相等的,這明顯不是我們想要的結果。正確的分工是,equals 負責判斷元素唯一性,compareTo 負責元素的排序,兩者互不干擾

下面以 TreeSet 為例,TreeSet 的 add 方法基於 TreeMap 的 put 方法實現,TreeMap 的結構是一顆紅黑樹,會根據預設比較器一直向下迭代,直到某個節點的左子樹或右子樹為 null,並將元素插入到該節點的左子樹或右子樹,並對整棵樹重寫進行顏色繪製。如果發現樹中某個節點的值和待插入元素元素一致,則覆蓋並返回舊值。回到 TreeSet 的 add 方法,put 方法的返回值不為 null,自然 add 方法的返回值就是 false

// TreeSet 中的 add 方法,基於 TreeMap 的 put 方法實現
public boolean add(E e) {
    return m.put(e, PRESENT) == null;
}

// TreeMap 中的 put 方法,這裡我們只關注被註釋的那一段程式碼即可
public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); 
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        do {
            parent = t;
            cmp = cpr.compare(key, t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    // 這裡使用 compareTo 對元素作自然排序
    else {
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                // 就是在這裡遇到相等的元素(根據比較器比較)
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

Comparator

Comparator 位於 java.util 包下,也是用來排序的。與 Comparable 不同的是,Comparable 表示該類“可以支援排序”,自身提供了排序方法;而 Comparator 則是一個“比較器”,這個比較器需要實現 Comparator 介面,可以通過這個比較器來對類排序,類本身不需要任何操作

當需要作排序操作如 Collections.sort 或是 Arrays.sort 時,把比較器作為引數傳進去即可。也可以使用 Comparator 來控制某些集合(TreeSet 或 TreeMap),如果集合要被序列化,Comparator 比較器也必須實現序列化介面

所以說,Comparator 和 Comparable 本質上沒有什麼區別,Comparable 要注意的點在 Comparator 中亦是如此


Comparator 的使用

自定義一個 User 實體類

public class User {
    private Integer id;
    private Integer age;

    // 構造方法和 set/get 方法省略 ...
}

自定義比較器

class AgeComparator implements Comparator<User> {
    
    @Override
    public int compare(User u1, User u2) {
        if (u1.getAge() > u2.getAge()) {
            return 1;
        } else if (u1.getAge() < u2.getAge()) {
            return -1;
        } else {
            return 0;
        }
    }
}

要使用比較器,只需要直接建立即可。也可以使用匿名內部類或者 lambda 表示式

// 已經定義了比較器,可直接使用
Collections.sort(list, new AgeComparator());
// 使用匿名內部類
Collections.sort(list, new Comparator<User>() {
    @Override
    public int compare(User u1, User u2) {
        ...
    }
});
// 使用 lambda 表示式
Collections.sort(list, (u1, u2) -> {...});

Comparator 中常用的預設方法

相比於 Comparable,Comparator 提供了更多預設方法和靜態方法,功能更加強大

  • reversed

    返回一個比較器,是原比較器的逆序(沒有實現則是自然排序),底層使用 Collections 的 reverseOrder 方法實現

    default Comparator<T> reversed() {
        return Collections.reverseOrder(this);
    }
    
  • comparing

    返回一個比較器,比較規則由傳入的引數制定,該方法有兩個過載方法

    // 引數為要比較的元素型別,預設按自然排序比較
    public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor)
    // 第一個引數為要比較的元素型別,第二個引數為比較規則
    public static <T, U> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor, Comparator<? super U> keyComparator)
    

    具體用法如下:

    Collections.sort(list, Comparator.comparing(User::getAge));
    Collections.sort(list, Comparator.comparing(Student::getLikeGame, Comparator.reverseOrder()));
    
  • thenComparing

    多條件排序的方法,當我們排序的條件不止一個的時候可以使用該方法。比如說我們對 User 先按照 age 欄位排序,再按照 id 排序,就可以使用 thenComparing 方法

    Collections.sort(list, comparator.thenComparing(x -> x.getId()));
    

    thenComparing 有很多過載方法,功能都一樣的,但有一點要注意:傳進去的型別都是按照自然排序,id 是一個整數,規則就是 1234 從小到大排序。如果你傳進去的是一個物件,而你希望能自定義比較規則,那麼這個物件必須實現 Comparable 介面

  • nullsFirst 和 nullsLast

    這兩個方法的意思是,如果排序的欄位為 null 的情況下,這條記錄該如何處理。nullsFirst 是將這條記錄排在最前面,而 nullsLast 是將這條記錄排序在最後面。如果多個 key 都為 null 的話,將無法保證這幾個物件的排序

    Comparator<User> comparator = Comparator.comparing(User::getAge, Comparator.nullsLast(Comparator.reverseOrder()));
    
  • reverseOrder 和 naturalOrder

    返回自然排序的比較器,reverseOrder 則是逆序。同樣的,對於自然排序,如果希望自定義規則,必須實現 Comparable 介面

    Collections.sort(list, Comparator.reverseOrder());
    

相關文章