本文基於 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());