Effective Java 第三版讀書筆記——條款14:考慮實現 Comparable 接口
與本章討論的其他方法不同,compareTo
方法並沒有在 Object
類中聲明。相反,它是 Comparable
接口中的唯一方法。 通過實現 Comparable
接口,一個類表明它的實例有一個自然序( natural ordering )。對實現 Comparable
接口的對象所組成的數組排序非常簡單,如下所示:
Arrays.sort(a);
通過實現 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
不一致”。
我們來仔細看一下 compareTo
約定的內容。第一條約定,如果反轉兩個對象引用之間的比較方向,則會發生預期的事情:如果第一個對象小於第二個對象,那麽第二個對象必須大於第一個;如果第一個對象等於第二個,那麽第二個對象必須等於第一個;如果第一個對象大於第二個,那麽第二個必須小於第一個。第二條約定說,如果一個對象大於第二個對象,而第二個對象大於第三個對象,則第一個對象必須大於第三個對象。最後一條約定,所有比較相等的對象與任何其他對象相比,都必須得到相同的結果。
compareTo
約定的最後一段是一個強烈的建議,而不是一個真正的要求,只是聲明 compareTo
方法執行的相等性測試,通常應該返回與 equals
方法相同的結果。如果遵守這個約定,則 compareTo
方法施加的順序被認為與 equals
相一致。如果違反,則這個順序關系被認為與 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
方法。可以編寫自己的比較器或使用現有的比較器,如在條款 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, cis.s);
}
... // Remainder omitted
}
在本書前兩版中曾經推薦如果比較整型基本類型的屬性,使用關系運算符 < 和 >,對於浮點型基本類型的屬性,使用 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, pn.areaCode);
if (result == 0) {
result = Short.compare(prefix, pn.prefix);
if (result == 0)
result = Short.compare(lineNum, pn.lineNum);
}
return result;
}
在 Java 8 中 Comparator
接口提供了一系列比較器方法,可以流暢地構建比較器。許多程序員更喜歡這種方法的簡潔性,盡管它會犧牲一定地性能。在使用這種方法時,考慮使用 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, pn);
}
此實現在類初始化時構建比較器,使用了兩個比較器構建方法。第一個是 comparingInt
方法。它是一個靜態方法,使用一個鍵提取器函數( key extractor function)作為參數,將對象引用映射為 int 類型的鍵,並返回一個根據該鍵對實例進行排序的比較器。在前面的示例中,comparingInt
方法使用 lambda 表達式,它從 PhoneNumber
中提取區域(area)代碼,並返回一個 Comparator<PhoneNumber>
,根據它們的區域代碼來對電話號碼排序。註意,lambda 表達式顯式指定了其輸入參數的類型 (PhoneNumber pn)
。事實證明,在這種情況下,Java 的類型推斷功能還不夠強大,無法自行判斷類型,因此我們不得不幫助它以使程序編譯。
如果兩個電話號碼實例具有相同的區號,則需要進一步細化比較,這正是第二個比較器構建方法,即 thenComparingInt
方法做的。它是 Comparator
上的一個實例方法,接受一個 int 類型鍵提取器函數作為參數,並返回一個比較器,該比較器首先應用原始比較器,然後使用提取的鍵來打破連接。你可以按照喜歡的方式多次調用 thenComparingInt
方法,從而產生一個字典順序。在上面的例子中,我們調用兩個 thenComparingInt
方法來產生一個排序,它的二級鍵是 prefix
,三級鍵是 lineNum
。請註意,我們不必指定傳遞給 thenComparingInt
方法中鍵提取器函數的參數類型:Java 的類型推斷足夠聰明,可以自己推斷出參數的類型。
Comparator
類具有完整的構建方法。對於 long
和 double
基本類型,也有對應的類似於 comparingInt
和 thenComparingInt的
的方法,int
版本的方法也可以應用在取值範圍小於 int
的類型上,如 short
類型。double
版本的方法也可以用在 float
類型上。這提供了對所有 Java 基本數值類型的覆蓋。
有時,你可能會看到 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浮點運算失真。應該使用靜態 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());
Effective Java 第三版讀書筆記——條款14:考慮實現 Comparable 接口