聊聊 Java String 源碼的排序算法

聊聊 Java String 源碼的排序算法

前言

Q:什麼是選擇問題? 選擇問題,是假設一組 N 個數,要確定其中第 K 個最大值者。比如 A 與 B 對象需要哪個更大?又比如:要考慮從一些數組中找出最大項?

解決選擇問題,需要對象有個能力,即比較任意兩個對象,並確定哪個大,哪個小或者相等。找出最大項問題的解決方法,只要依次用對象的比較(Comparable)能力,循環對象列表,一次就能解決。

那麼 JDK 源碼如何實現比較(Comparable)能力的呢?

java.lang.Comparable 接口

Comparable 接口,從 JDK 1.2 版本就有了,歷史算悠久。Comparable 接口強制了實現類對象列表的排序。其排序稱爲自然順序,其 compareTo 方法,稱爲自然比較法。

該接口只有一個方法 public int compareTo(T o); ,可以看出:

  • 入參 T o :實現該接口類,傳入對應的要被比較的對象;
  • 返回值 int:正數、負數和 0 ,代表大於、小於和等於。

對象的集合列表(Collection List)或者數組(arrays) ,也有對應的工具類可以方便的使用:

  • java.util.Collections#sort(List) 列表排序
  • java.util.Arrays#sort(Object[]) 數組排序

那 String 對象如何被比較的?

String 源碼中的算法

String 源碼中可以看到 String JDK 1.0 就有了。那麼應該是 JDK 1.2 的時候,String 類實現了 Comparable 接口,並且傳入需要被比較的對象是 String。對象如圖:

/\*\* \* 字符串比較案例 \* \* Created by bysocket @ bysocket.com on 19/5/10\. \*/
public class StringComparisonDemo {

    public static void main(String[] args) {
        String foo = "ABC";

        // 前面和後面每個字符完全一樣,返回 0
        String bar01 = "ABC";
        System.out.println(foo.compareTo(bar01));

        // 前面每個字符完全一樣,返回:後面就是字符串長度差
        String bar02 = "ABCD";
        String bar03 = "ABCDE";
        System.out.println(foo.compareTo(bar02)); // -1 (前面相等,foo 長度小 1)
        System.out.println(foo.compareTo(bar03)); // -2 (前面相等,foo 長度小 2)

        // 前面每個字符不完全一樣,返回:出現不一樣的字符 ASCII 差
        String bar04 = "ABD";
        String bar05 = "aABCD";
        System.out.println(foo.compareTo(bar04)); // -1 (foo 的 'C' 字符 ASCII 碼值爲 67,bar04 的 'D' 字符 ASCII 碼值爲 68。返回 67 - 68 = -1)
        System.out.println(foo.compareTo(bar05)); // -32 (foo 的 'A' 字符 ASCII 碼值爲 65,bar04 的 'a' 字符 ASCII 碼值爲 97。返回 65 - 97 = -32)

        String bysocket01 = "泥瓦匠";
        String bysocket02 = "瓦匠";
        System.out.println(bysocket01.compareTo(bysocket02));// -2049 (泥 和 瓦的 Unicode 差值)
    }
}

運行結果如下:

0
-1
-2
-1
-32
-2049

可以看出, compareTo 方法是按字典順序比較兩個字符串。具體比較規則可以看代碼註釋。比較規則如下:

  • 字符串的每個字符完全一樣,返回 0;
  • 字符串前面部分的每個字符完全一樣,返回:後面就是兩個字符串長度差;
  • 字符串前面部分的每個字符存在不一樣,返回:出現不一樣的字符 ASCII 碼的差值。
    • 中文比較返回對應的 Unicode 編碼值(Unicode 包含 ASCII)
    • foo 的 ‘C’ 字符 ASCII 碼值爲 67
    • bar04 的 ‘D’ 字符 ASCII 碼值爲 68。
    • foo.compareTo(bar04),返回 67 - 68 = -1
    • 常見字符 ASCII 碼,如圖所示

再看看 String 的

源碼解析如下:

  • 第 1156 行:獲取當前字符串和另一個字符串,長度較小的長度值 lim;
  • 第 1169 行:當前字符串和另一個字符串,依次字符比較。如果均相等,則返回兩個字符串長度的差值。

所以要排序,肯定先有比較能力,即實現 Comparable 接口。然後實現此接口的對象列表(和數組)可以通過 Collections.sort(和 Arrays.sort)進行排序。

還有 TreeSet 使用樹結構實現(紅黑樹),集合中的元素進行排序。其中排序就是實現 Comparable 此接口

另外,如果沒有實現 Comparable 接口,使用排序時,會拋出 java.lang.ClassCastException 異常。詳細看《Java 集合:三、HashSet,TreeSet 和 LinkedHashSet比較》https://www.bysocket.com/archives/195

小結

上面也說到,這種比較其實有一定的弊端:

  • 默認 compareTo 不忽略字符大小寫。如果需要忽略,則重新自定義 compareTo 方法;
  • 無法進行二維的比較決策。比如判斷 2 * 1 矩形和 3 * 3 矩形,哪個更大?
  • 比如有些類無法實現該接口。一個 final 類,也無法擴展新的類。其也有解決方案:函數對象(Function Object)。

方法參數:定義一個沒有數據只有方法的類,並傳遞該類的實例。一個函數通過將其放在一個對象內部而被傳遞,這種對象通常叫做函數對象(Funtion Object)。

在接口方法設計中, T execute(Callback callback) 參數中使用 callback 類似。比如在 Spring 源碼中,可以看出很多設計是:聚合優先於繼承或者實現。這樣可以減少很多繼承或者實現。類似 SpringJdbcTemplate 場景設計,可以考慮到這種 Callback 設計實現。


歡迎關注我的公衆號,回覆關鍵字“Java” ,將會有大禮相送!!! 祝各位面試成功!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章