Effective Java 第三版——14.考慮實現Comparable接口
Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨著Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這裏第一時間翻譯成中文版。供大家學習分享之用。
?14.考慮實現Comparable接口
與本章討論的其他方法不同,compareTo
方法並沒有在Object
類中聲明。 相反,它是Comparable
接口中的唯一方法。 它與Object
類的equals
方法在性質上是相似的,除了它允許在簡單的相等比較之外的順序比較,它是泛型的。 通過實現Comparable
Comparable
接口的對象數組排序非常簡單,如下所示:
Arrays.sort(a);
它很容易查找,計算極端數值,以及維護Comparable
對象集合的自動排序。例如,在下面的代碼中,依賴於String
類實現了Comparable
接口,去除命令行參數輸入重復的字符串,並按照字母順序排序:
public class WordList { ????public static void main(String[] args) { ????????Set<String> s = new TreeSet<>(); ????????Collections.addAll(s, args); ????????System.out.println(s); ????} }
通過實現Comparable
接口,可以讓你的類與所有依賴此接口的通用算法和集合實現進行互操作。 只需少量的努力就可以獲得巨大的能量。 幾乎Java平臺類庫中的所有值類以及所有枚舉類型(條目 34)都實現了Comparable
接口。 如果你正在編寫具有明顯自然順序(如字母順序,數字順序或時間順序)的值類,則應該實現Comparable
接口:
public interface Comparable<T> {
????int compareTo(T t);
}
compareTo
方法的通用約定與equals
相似:
將此對象與指定的對象按照排序進行比較。 返回值可能為負整數,零或正整數,因為此對象對應小於,等於或大於指定的對象。 如果指定對象的類型與此對象不能進行比較,則引發ClassCastException
下面的描述中,符號sgn(expression)表示數學中的 signum 函數,它根據表達式的值為負數、零、正數,對應返回-1、0和1。
- 實現類必須確保所有
x
和y
都滿足sgn(x.compareTo(y)) == -sgn(y. compareTo(x))
。 (這意味著當且僅當y.compareTo(x)
拋出異常時,x.compareTo(y)
必須拋出異常。) - 實現類還必須確保該關系是可傳遞的:
(x. compareTo(y) > 0 && y.compareTo(z) > 0)
意味著x.compareTo(z) > 0
。 最後,對於所有的z,實現類必須確保
[x.compareTo(y) == 0
意味著sgn(x.compareTo(z)) == sgn(y.compareTo(z))
。強烈推薦
x.compareTo(y) == 0) == (x.equals(y))
,但不是必需的。 一般來說,任何實現了Comparable
接口的類違反了這個條件都應該清楚地說明這個事實。 推薦的語言是“註意:這個類有一個自然順序,與equals
不一致”。
與equals
方法一樣,不要被上述約定的數學特性所退縮。這個約定並不像看起來那麽復雜。 與equals
方法不同,equals
方法在所有對象上施加了全局等價關系,compareTo
不必跨越不同類型的對象:當遇到不同類型的對象時,compareTo
被允許拋出ClassCastException
異常。 通常,這正是它所做的。 約定確實允許進行不同類型間比較,這種比較通常在由被比較的對象實現的接口中定義。
正如一個違反hashCode
約定的類可能會破壞依賴於哈希的其他類一樣,違反compareTo
約定的類可能會破壞依賴於比較的其他類。 依賴於比較的類,包括排序後的集合TreeSet
和TreeMap
類,以及包含搜索和排序算法的實用程序類Collections
和Arrays
。
我們來看看compareTo
約定的規定。 第一條規定,如果反轉兩個對象引用之間的比較方向,則會發生預期的事情:如果第一個對象小於第二個對象,那麽第二個對象必須大於第一個; 如果第一個對象等於第二個,那麽第二個對象必須等於第一個; 如果第一個對象大於第二個,那麽第二個必須小於第一個。 第二項約定說,如果一個對象大於第二個對象,而第二個對象大於第三個對象,則第一個對象必須大於第三個對象。 最後一條規定,所有比較相等的對象與任何其他對象相比,都必須得到相同的結果。
這三條規定的一個結果是,compareTo
方法所實施的平等測試必須遵守equals
方法約定所施加的相同限制:自反性,對稱性和傳遞性。 因此,同樣需要註意的是:除非你願意放棄面向對象抽象(條目 10)的好處,否則無法在保留compareTo
約定的情況下使用新的值組件繼承可實例化的類。 同樣的解決方法也適用。 如果要將值組件添加到實現Comparable
的類中,請不要繼承它;編寫一個包含第一個類實例的不相關的類。 然後提供一個返回包含實例的“視圖”方法。 這使你可以在包含類上實現任何compareTo
方法,同時客戶端在需要時,把包含類的實例視同以一個類的實例。
compareTo
約定的最後一段是一個強烈的建議,而不是一個真正的要求,只是聲明compareTo
方法施加的相等性測試,通常應該返回與equals
方法相同的結果。 如果遵守這個約定,則compareTo
方法施加的順序被認為與equals
相一致。 如果違反,順序關系被認為與equals
不一致。 其compareTo
方法施加與equals
不一致順序關系的類仍然有效,但包含該類元素的有序集合可能不服從相應集合接口(Collection
,Set
或Map
)的一般約定。 這是因為這些接口的通用約定是用equals
方法定義的,但是排序後的集合使用compareTo
強加的相等性測試來代替equals
。 如果發生這種情況,雖然不是一場災難,但仍是一件值得註意的事情。
例如,考慮BigDecimal
類,其compareTo
方法與equals
不一致。 如果你創建一個空的HashSet
實例,然後添加new BigDecimal("1.0")
和new BigDecimal("1.00")
,則該集合將包含兩個元素,因為與equals
方法進行比較時,添加到集合的兩個BigDecimal
實例是不相等的。 但是,如果使用TreeSet
而不是HashSet
執行相同的過程,則該集合將只包含一個元素,因為使用compareTo
方法進行比較時,兩個BigDecimal
實例是相等的。 (有關詳細信息,請參閱BigDecimal
文檔。)
編寫compareTo
方法與編寫equals
方法類似,但是有一些關鍵的區別。 因為Comparable
接口是參數化的,compareTo
方法是靜態類型的,所以你不需要輸入檢查或者轉換它的參數。 如果參數是錯誤的類型,那麽調用將不會編譯。 如果參數為null,則調用應該拋出一個NullPointerException
異常,並且一旦該方法嘗試訪問其成員,它就會立即拋出這個異常。
在compareTo
方法中,比較屬性的順序而不是相等。 要比較對象引用屬性,請遞歸調用compareTo
方法。 如果一個屬性沒有實現Comparable
,或者你需要一個非標準的順序,那麽使用Comparator
接口。 可以編寫自己的比較器或使用現有的比較器,如在條目 10中的CaseInsensitiveString
類的compareTo
方法中:
// Single-field Comparable with object reference field
public final class CaseInsensitiveString
????????implements Comparable<CaseInsensitiveString> {
????public int compareTo(CaseInsensitiveString cis) {
????????return String.CASE_INSENSITIVE_[ORDER.compare(s](http://ORDER.compare(s), cis.s);
????}
????... // Remainder omitted
}
請註意,CaseInsensitiveString
類實現了Comparable <CaseInsensitiveString>
接口。 這意味著CaseInsensitiveString
引用只能與另一個CaseInsensitiveString
引用進行比較。 當聲明一個類來實現Comparable
接口時,這是正常模式。
在本書第二版中,曾經推薦如果比較整型基本類型的屬性,使用關系運算符“<” 和 “>”,對於浮點類型基本類型的屬性,使用Double.compare
和[Float.compare
靜態方法。在Java 7中,靜態比較方法被添加到Java的所有包裝類中。 在compareTo
方法中使用關系運算符“<” 和“>”是冗長且容易出錯的,不再推薦。
如果一個類有多個重要的屬性,那麽比較他們的順序是至關重要的。 從最重要的屬性開始,逐步比較所有的重要屬性。 如果比較結果不是零(零表示相等),則表示比較完成; 只是返回結果。 如果最重要的字段是相等的,比較下一個重要的屬性,依此類推,直到找到不相等的屬性或比較剩余不那麽重要的屬性。 以下是條目 11中PhoneNumber
類的compareTo
方法,演示了這種方法:
// Multiple-field Comparable with primitive fields
public int compareTo(PhoneNumber pn) {
????int result = [Short.compare(areaCode](http://Short.compare(areaCode), pn.areaCode);
????if (result == 0)??{
????????result = [Short.compare(prefix](http://Short.compare(prefix), pn.prefix);
????????if (result == 0)
????????????result = [Short.compare(lineNum](http://Short.compare(lineNum), pn.lineNum);
????}
????return result;
}
在Java 8中Comparator
接口提供了一系列比較器方法,可以使比較器流暢地構建。 這些比較器可以用來實現compareTo
方法,就像Comparable
接口所要求的那樣。 許多程序員更喜歡這種方法的簡潔性,盡管它的性能並不出眾:在我的機器上排序PhoneNumber
實例的數組速度慢了大約10%。 在使用這種方法時,考慮使用Java的靜態導入,以便可以通過其簡單名稱來引用比較器靜態方法,以使其清晰簡潔。 以下是PhoneNumber
的compareTo
方法的使用方法:
// Comparable with comparator construction methods
private static final Comparator<PhoneNumber> COMPARATOR =
????????comparingInt((PhoneNumber pn) -> pn.areaCode)
??????????.thenComparingInt(pn -> pn.prefix)
??????????.thenComparingInt(pn -> pn.lineNum);
public int compareTo(PhoneNumber pn) {
????return [COMPARATOR.compare(this](http://COMPARATOR.compare(this), pn);
}
此實現在類初始化時構建比較器,使用兩個比較器構建方法。第一個是comparingInt
方法。它是一個靜態方法,它使用一個鍵提取器函數式接口(?key extractor function)作為參數,將對象引用映射為int類型的鍵,並返回一個根據該鍵排序的實例的比較器。在前面的示例中,comparingInt
方法使用lambda表達式,它從PhoneNumber
中提取區域代碼,並返回一個Comparator<PhoneNumber>
,根據它們的區域代碼來排序電話號碼。註意,lambda表達式顯式指定了其輸入參數的類型(PhoneNumber pn)
。事實證明,在這種情況下,Java的類型推斷功能不夠強大,無法自行判斷類型,因此我們不得不幫助它以使程序編譯。
如果兩個電話號碼實例具有相同的區號,則需要進一步細化比較,這正是第二個比較器構建方法,即thenComparingInt
方法做的。 它是Comparator
上的一個實例方法,接受一個int類型鍵提取器函數式接口(?key extractor function)作為參數,並返回一個比較器,該比較器首先應用原始比較器,然後使用提取的鍵來打破連接。 你可以按照喜歡的方式多次調用thenComparingIn
t方法,從而產生一個字典順序。 在上面的例子中,我們將兩個調用疊加到thenComparingInt
,產生一個排序,它的二級鍵是prefix
,而其三級鍵是lineNum
。 請註意,我們不必指定傳遞給thenComparingInt
的任何一個調用的鍵提取器函數式接口的參數類型:Java的類型推斷足夠聰明,可以自己推斷出參數的類型。
Comparator
類具有完整的構建方法。對於long
和double
基本類型,也有對應的類似於comparingInt
和thenComparingInt的
方法,int
版本的方法也可以應用於取值範圍小於 int
的類型上,如short
類型,如PhoneNumber
實例中所示。對於double
版本的方法也可以用在float
類型上。這提供了所有Java的基本數字類型的覆蓋。
也有對象引用類型的比較器構建方法。靜態方法comparing
有兩個重載方式。第一個方法使用鍵提取器函數式接口並按鍵的自然順序。第二種方法是鍵提取器函數式接口和比較器,用於鍵的排序。thenComparing
方法有三種重載。第一個重載只需要一個比較器,並使用它來提供一個二級排序。第二次重載只需要一個鍵提取器函數式接口,並使用鍵的自然順序作為二級排序。最後的重載方法同時使用一個鍵提取器函數式接口和一個比較器來用在提取的鍵上。
有時,你可能會看到compareTo
或compare
方法依賴於兩個值之間的差值,如果第一個值小於第二個值,則為負;如果兩個值相等則為零,如果第一個值大於,則為正值。這是一個例子:
// BROKEN difference-based comparator - violates transitivity!
static Comparator<Object> hashCodeOrder = new Comparator<>() {
public int compare(Object o1, Object o2) {
return o1.hashCode() - o2.hashCode();
}
};
不要使用這種技術!它可能會導致整數最大長度溢出和IEEE 754浮點運算失真的危險[JLS 15.20.1,15.21.1]。 此外,由此產生的方法不可能比使用上述技術編寫的方法快得多。 使用靜態compare
方法:
**// Comparator based on static compare method**
static Comparator<Object> hashCodeOrder = new Comparator<>() {
????public int compare(Object o1, Object o2) {
????????return Integer.compare(o1.hashCode(), o2.hashCode());
????}
};
或者使用Comparator
的構建方法:
// Comparator based on Comparator construction method
static Comparator<Object> hashCodeOrder =
Comparator.comparingInt(o -> o.hashCode());
總而言之,無論何時實現具有合理排序的值類,你都應該讓該類實現Comparable
接口,以便在基於比較的集合中輕松對其實例進行排序,搜索和使用。 比較compareTo
方法的實現中的字段值時,請避免使用"<"和">"運算符。 相反,使用包裝類中的靜態compare
方法或Comparator
接口中的構建方法。
Effective Java 第三版——14.考慮實現Comparable接口