1. 程式人生 > >Java Comparable 和 Comparator 介面詳解

Java Comparable 和 Comparator 介面詳解


> 本文基於 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 中的原始碼 ```java // T 是可比較的型別 public interface Comparable { public int compareTo(T o); } ``` 需要比較的類只需實現 Comparable 介面即可,在 compareTo 中定義自己的比較規則 - 返回 0 表示當前物件與目標物件相等 - 返回正數表示當前物件比目標物件大 - 返回負數表示當前物件比目標物件小 ```java public class User implements Comparable{ 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 ```java // 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 t = root; if (t == null) { compare(key, key); root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry parent; Comparator 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 k = (Comparable) 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 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 實體類 ```java public class User { private Integer id; private Integer age; // 構造方法和 set/get 方法省略 ... } ``` 自定義比較器 ```java class AgeComparator implements Comparator { @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 表示式 ```java // 已經定義了比較器,可直接使用 Collections.sort(list, new AgeComparator()); // 使用匿名內部類 Collections.sort(list, new Comparator() { @Override public int compare(User u1, User u2) { ... } }); // 使用 lambda 表示式 Collections.sort(list, (u1, u2) -> {...}); ```
## Comparator 中常用的預設方法 相比於 Comparable,Comparator 提供了更多預設方法和靜態方法,功能更加強大 - reversed 返回一個比較器,是原比較器的逆序(沒有實現則是自然排序),底層使用 Collections 的 reverseOrder 方法實現 ```java default Comparator reversed() { return Collections.reverseOrder(this); } ``` - comparing 返回一個比較器,比較規則由傳入的引數制定,該方法有兩個過載方法 ```java // 引數為要比較的元素型別,預設按自然排序比較 public static > Comparator comparing(Function keyExtractor) // 第一個引數為要比較的元素型別,第二個引數為比較規則 public static Comparator comparing(Function keyExtractor, Comparator keyComparator) ``` 具體用法如下: ```java Collections.sort(list, Comparator.comparing(User::getAge)); Collections.sort(list, Comparator.comparing(Student::getLikeGame, Comparator.reverseOrder())); ``` - thenComparing 多條件排序的方法,當我們排序的條件不止一個的時候可以使用該方法。比如說我們對 User 先按照 age 欄位排序,再按照 id 排序,就可以使用 thenComparing 方法 ```java Collections.sort(list, comparator.thenComparing(x -> x.getId())); ``` thenComparing 有很多過載方法,功能都一樣的,但有一點要注意:傳進去的型別都是按照自然排序,id 是一個整數,規則就是 1234 從小到大排序。如果你傳進去的是一個物件,而你希望能自定義比較規則,那麼這個物件必須實現 Comparable 介面 - nullsFirst 和 nullsLast 這兩個方法的意思是,如果排序的欄位為 null 的情況下,這條記錄該如何處理。nullsFirst 是將這條記錄排在最前面,而 nullsLast 是將這條記錄排序在最後面。如果多個 key 都為 null 的話,將無法保證這幾個物件的排序 ```java Comparator comparator = Comparator.comparing(User::getAge, Comparator.nullsLast(Comparator.reverseOrder())); ``` - reverseOrder 和 naturalOrder 返回自然排序的比較器,reverseOrder 則是逆序。同樣的,對於自然排序,如果希望自定義規則,必須實現 Comparable 介面 ```java Collections.sort(list, Comparator.reverseOrder()); ```