程式兵法:Java String 原始碼的排序演算法(一)
這是泥瓦匠的第103篇原創
《程式兵法:Java String 原始碼的排序演算法(一)》
文章工程:
* JDK 1.8
* 工程名:algorithm-core-learning # StringComparisonDemo
* 工程地址:https://github.com/JeffLi1993/algorithm-core-learning
一、前言
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。物件如圖:
String 是一個 final 類,無法從 String 擴充套件新的類。從 114 行,可以看出字串的儲存結構是字元(Char)陣列。先可以看看一個字串比較案例,程式碼如下:
/**
* 字串比較案例
*
* Created by bysocket 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 的 compareTo
方法如何實現字典順序的。原始碼如圖:
原始碼解析如下:
- 第 1156 行:獲取當前字串和另一個字串,長度較小的長度值 lim
- 第 1161 行:如果 lim 大於 0 (較小的字串非空),則開始比較
- 第 1164 行:當前字串和另一個字串,依次字元比較。如果不相等,則返回兩字元的 Unicode 編碼值的差值
- 第 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 設計實現。
程式碼示例
本文示例讀者可以通過檢視下面倉庫的中: StringComparisonDemo 字串比較案例案例:
- Github:https://github.com/JeffLi1993/algorithm-core-learning
- Gitee:https://gitee.com/jeff1993/algorithm-core-learning
如果您對這些感興趣,歡迎 star、follow、收藏、轉發給予支援!
參考資料
- 《資料結構與演算法分析:Java語言描述(原書第3版)》
- https://en.wikipedia.org/wiki/Unicode
- https://www.cnblogs.com/vamei/tag/%E7%AE%97%E6%B3%95/
- https://www.bysocket.com/archives/2314/algorithm
以下專題教程也許您會有興趣
- 《程式兵法:演算法與資料結構》 https://www.bysocket.com/archives/2314/algorithm
- 《Spring Boot 2.x 系列教程》
https://www.bysocket.com/springboot - 《Java 核心系列教程》
https://www.bysocket.com/archives/2100
(關注微信公眾號,領取 Java 精選乾貨學習資料)
(新增我微信:bysocket01。加入純技術交流群,成長技術