一、前言
在Java集合框架裡面,各種集合的操作很大程度上都離不開Comparable和Comparator,雖然它們與集合沒有顯示的關係,但是它們只有在集合裡面的時候才能發揮最大的威力。下面是開始我們的分析。
二、示例
在正式講解Comparable與Comparator之前,我們通過一個例子來直觀的感受一下它們的使用。
首先,定義好我們的Person類
class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } public String toString() { return "[name = " + name + ", age = " + age + "]"; } }
其次編寫測試程式碼,程式碼如下
public class Test { public static void main(String[] args) { List<String> nameLists = new ArrayList<String>(); nameLists.addAll(Arrays.asList("aa", "ab", "bc", "ba")); Collections.sort(nameLists); System.out.println(nameLists); List<Person> personLists = new ArrayList<Person>();
personLists.addAll(Arrays.asList(new Person("leesf", 24), new Person("dyd", 24), new Person("ld", 0)));
Collections.sort(personLists); // 出錯
System.out.println(personLists); } }
說明:上述程式碼是兩份同樣的邏輯,同樣的操作,但是,對於List<String>不會報錯,對於List<Person>型別就會報錯,為什麼?為了解決這個問題,我們需要講解今天的主角Comparable && Comparator。如果知道怎麼解決的園友也不妨瞧瞧,開始分析。
三、原始碼分析
3.1 Comparable
1. 類的繼承關係
public interface Comparable<T>
說明:Comparable就是一個泛型介面,很簡單。
2. compareTo方法
public int compareTo(T o);
說明:compareTo方法就構成了整個Comparable原始碼的唯一的有效方法。
3.2 Comparator
1. 類的繼承關係
public interface Comparator<T>
說明:同樣,Comparator也是一個泛型介面,很簡單。
2. compare方法
int compare(T o1, T o2);
說明:Comparator介面中一個核心的方法。
3. equals方法
boolean equals(Object obj);
說明:此方法是也是一個比較重要的方法,但是一般不會使用,可以直接使用Object物件的equals方法(所有物件都繼承自Object)。
其他在JDK1.8後新增的方法對我們的分析不產生影響,有感興趣的讀者可以自行閱讀原始碼,瞭解更多細節。
四、解決思路
4.1. 分析問題
在我們的程式中,List<String>型別是可以通過編譯的,但是List<Person>型別卻不行,我們猜測肯定是和元素型別String、Person有關係。既然是這樣,我們來看String在Java中的定義。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
說明:我們平時說String為final型別,不可被繼承,檢視原始碼,確實是這樣。注意檢視String實現的介面,直覺告訴我們Comparable<String>很重要,之前我們已經分析過了Comparable介面,既然String實現了這個介面,那麼肯定也實現了compareTo方法,順藤摸瓜,String的compareTo方法如下:
public int compareTo(String anotherString) { // this物件所對應的字串的長度 int len1 = value.length; // 引數物件所對應字串的長度 int len2 = anotherString.value.length; // 取長度較小者 int lim = Math.min(len1, len2); // value是String底層的實現,為char[]型別陣列 // this物件所對應的字串 char v1[] = value; // 引數物件所對應的字串 char v2[] = anotherString.value; int k = 0; // 遍歷兩個字串 while (k < lim) { char c1 = v1[k]; char c2 = v2[k]; // 如果不相等,則返回 if (c1 != c2) { return c1 - c2; } // 繼續遍歷 k++; } // 一個字串是另外一個字串的子串 return len1 - len2; }
說明:我們可以看到String中compareTo方法具體的實現。比較同一索引位置的字元大小。
分析了String的compareTo方法後,並且按照在compareTo方法中的邏輯進行排序,之於如何排序涉及到具體的演算法問題,以後我們會進行分析。於是乎,我們知道了之前示例程式的問題所在:Person類沒有實現Comparable介面。
4.2. 解決問題
1. 修改我們的Person類的定義,修改為如下:
Person implements Comparable<Person>
2. 實現compareTo方法,並實現我們自己的想要比較的邏輯,如我們想要首先根據年齡比較(採用升序),若年齡相同,則根據姓名的ASCII順序來比較。那麼我們實現的compareTo方法如下:
int compareTo(Person anthor) { if (this.age < anthor.age) return -1; else if (this.age == anthor.age) return this.name.compareTo(anthor.name); else return 1; }
說明:於是乎,修改後的程式如下:
Person類程式碼如下
class Person implements Comparable<Person> { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } public String toString() { return "[name = " + name + ", age = " + age + "]"; } @Override public int compareTo(Person anthor) { if (this.age < anthor.age) return -1; else if (this.age == anthor.age) return this.name.compareTo(anthor.name); else return 1; } }
測試類程式碼不變
執行結果如下:
[aa, ab, ba, bc]
[[name = ld, age = 0], [name = dyd, age = 24], [name = leesf, age = 24]]
說明:我們可以看到Person類的排序確實按照了在compareTo方法中定義的邏輯進行排序。這樣,就修正了錯誤。
五、問題提出
上面的Comparable介面解決之前出現的問題。但是,如果我現在不想按照剛剛的邏輯進行排序了,想按照一套新的邏輯排序,如只根據姓名比較來進行排序。此時,我們需要修改Comparable介面的compare方法,新增新的比較邏輯。過了一久,使用者又希望採用別的邏輯進行排序,那麼,又得重新修改compareTo方法裡面的邏輯,可以通過標誌位來做if判斷,用來判斷使用者想要使用哪種比較邏輯,這樣會造成會造成程式碼很臃腫,不易於維護。此時,一種更好的解決辦法就是使用Comparator介面。
5.1 比較邏輯一
首先根據年齡比較(採用升序),若年齡相同,則根據姓名的ASCII順序來比較。
那麼我們可以定義這樣的Comparator,具體程式碼如下:
class ComparatorFirst implements Comparator<Person> { public int compare(Person o1, Person o2) { if (o1.age < o2.age) return -1; else if (o1.age == o2.age) return o1.name.compareTo(o2.name); else return 1; } }
測試程式碼做如下修改:
將Collections.sort(personLists) 改成 Collections.sort(personLists, new ComparatorFirst());
sort的兩種過載方法,後一種允許我們傳入自定義的比較器。
執行結果如下:
[aa, ab, ba, bc]
[[name = ld, age = 0], [name = dyd, age = 24], [name = leesf, age = 24]]
結果說明:我們看到和前面使用Comparable介面得到的結果相同。
5.2 比較邏輯二
直接根據姓名的ASCII順序來比較。
則我們可以定義如下比較器
class ComparatorSecond implements Comparator<Person> { public int compare(Person o1, Person o2) { return o1.name.compareTo(o2.name); } }
測試程式碼做如下修改:
將Collections.sort(personLists) 改成 Collections.sort(personLists, new ComparatorSecond());
執行結果:
[aa, ab, ba, bc]
[[name = dyd, age = 24], [name = ld, age = 0], [name = leesf, age = 24]]
說明:我們可以看到這個比較邏輯和上一個比較器的邏輯不相同,但是也同樣完成了使用者的邏輯。
我們還可以按照我們的意願定義其他更多的比較器,只需要在compareTo中正確完成我們的邏輯即可。
5.3 Comparator優勢
從上面兩個例子我們應該可以感受到Comparator比較器比Comparable介面更加靈活,可以更友好的完成使用者所定義的各種比較邏輯。
六、總結
分析了Comparable和Comparator,掌握了在不同的場景中使用不同的比較器,寫此篇部落格後對兩者的使用和區別也更加的清晰了。謝謝各位園友的觀看~