【集合框架】JDK1.8原始碼分析之Comparable && Comparator(九)

leesf發表於2016-03-23

一、前言

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

  

  

  

  

  

  

  

 

  

相關文章