Effective Java 第三版——14.考慮實現Comparable接口

Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必不少人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到如今已經將近8年的時間,但隨着Java 6,7,8,甚至9的發佈,Java語言發生了深入的變化。
在這裏第一時間翻譯成中文版。供你們學習分享之用。程序員

Effective Java, Third Edition

 14.考慮實現Comparable接口

與本章討論的其餘方法不一樣,compareTo方法並無在Object類中聲明。 相反,它是Comparable接口中的惟一方法。 它與Object類的equals方法在性質上是類似的,除了它容許在簡單的相等比較以外的順序比較,它是泛型的。 經過實現Comparable接口,一個類代表它的實例有一個天然順序( natural ordering)。 對實現Comparable接口的對象數組排序很是簡單,以下所示:算法

Arrays.sort(a);

它很容易查找,計算極端數值,以及維護Comparable對象集合的自動排序。例如,在下面的代碼中,依賴於String類實現了Comparable接口,去除命令行參數輸入重複的字符串,並按照字母順序排序:express

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 函數,它根據表達式的值爲負數、零、正數,對應返回-一、0和1。學習

  • 實現類必須確保全部xy都知足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不一致」。this

equals方法同樣,不要被上述約定的數學特性所退縮。這個約定並不像看起來那麼複雜。 與equals方法不一樣,equals方法在全部對象上施加了全局等價關係,compareTo沒必要跨越不一樣類型的對象:當遇到不一樣類型的對象時,compareTo被容許拋出ClassCastException異常。 一般,這正是它所作的。 約定確實容許進行不一樣類型間比較,這種比較一般在由被比較的對象實現的接口中定義。命令行

正如一個違反hashCode約定的類可能會破壞依賴於哈希的其餘類同樣,違反compareTo約定的類可能會破壞依賴於比較的其餘類。 依賴於比較的類,包括排序後的集合TreeSetTreeMap類,以及包含搜索和排序算法的實用程序類CollectionsArrays

咱們來看看compareTo約定的規定。 第一條規定,若是反轉兩個對象引用之間的比較方向,則會發生預期的事情:若是第一個對象小於第二個對象,那麼第二個對象必須大於第一個; 若是第一個對象等於第二個,那麼第二個對象必須等於第一個; 若是第一個對象大於第二個,那麼第二個必須小於第一個。 第二項約定說,若是一個對象大於第二個對象,而第二個對象大於第三個對象,則第一個對象必須大於第三個對象。 最後一條規定,全部比較相等的對象與任何其餘對象相比,都必須獲得相同的結果。

這三條規定的一個結果是,compareTo方法所實施的平等測試必須遵照equals方法約定所施加的相同限制:自反性,對稱性和傳遞性。 所以,一樣須要注意的是:除非你願意放棄面向對象抽象(條目 10)的好處,不然沒法在保留compareTo約定的狀況下使用新的值組件繼承可實例化的類。 一樣的解決方法也適用。 若是要將值組件添加到實現Comparable的類中,請不要繼承它;編寫一個包含第一個類實例的不相關的類。 而後提供一個返回包含實例的「視圖」方法。 這使你能夠在包含類上實現任何compareTo方法,同時客戶端在須要時,把包含類的實例視同以一個類的實例。

compareTo約定的最後一段是一個強烈的建議,而不是一個真正的要求,只是聲明compareTo方法施加的相等性測試,一般應該返回與equals方法相同的結果。 若是遵照這個約定,則compareTo方法施加的順序被認爲與equals相一致。 若是違反,順序關係被認爲與equals不一致。 其compareTo方法施加與equals不一致順序關係的類仍然有效,但包含該類元素的有序集合可能不服從相應集合接口(CollectionSetMap)的通常約定。 這是由於這些接口的通用約定是用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的靜態導入,以即可以經過其簡單名稱來引用比較器靜態方法,以使其清晰簡潔。 如下是PhoneNumbercompareTo方法的使用方法:

// 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)做爲參數,並返回一個比較器,該比較器首先應用原始比較器,而後使用提取的鍵來打破鏈接。 你能夠按照喜歡的方式屢次調用thenComparingInt方法,從而產生一個字典順序。 在上面的例子中,咱們將兩個調用疊加到thenComparingInt,產生一個排序,它的二級鍵是prefix,而其三級鍵是lineNum。 請注意,咱們沒必要指定傳遞給thenComparingInt的任何一個調用的鍵提取器函數式接口的參數類型:Java的類型推斷足夠聰明,能夠本身推斷出參數的類型。

Comparator類具備完整的構建方法。對於longdouble基本類型,也有對應的相似於comparingIntthenComparingInt的方法,int版本的方法也能夠應用於取值範圍小於 int的類型上,如short類型,如PhoneNumber實例中所示。對於double版本的方法也能夠用在float類型上。這提供了全部Java的基本數字類型的覆蓋。

也有對象引用類型的比較器構建方法。靜態方法comparing有兩個重載方式。第一個方法使用鍵提取器函數式接口並按鍵的天然順序。第二種方法是鍵提取器函數式接口和比較器,用於鍵的排序。thenComparing方法有三種重載。第一個重載只須要一個比較器,並使用它來提供一個二級排序。第二次重載只須要一個鍵提取器函數式接口,並使用鍵的天然順序做爲二級排序。最後的重載方法同時使用一個鍵提取器函數式接口和一個比較器來用在提取的鍵上。

有時,你可能會看到compareTocompare方法依賴於兩個值之間的差值,若是第一個值小於第二個值,則爲負;若是兩個值相等則爲零,若是第一個值大於,則爲正值。這是一個例子:

// 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接口中的構建方法。

相關文章
相關標籤/搜索