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


一、前言

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

  

  

  

  

  

  

  

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM