一、前言
在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,掌握了在不同的場景中使用不同的比較器,寫此篇博客后對兩者的使用和區別也更加的清晰了。謝謝各位園友的觀看~