1. 程式人生 > >【Effective java 學習】第三章:對於所有物件都通用的方法

【Effective java 學習】第三章:對於所有物件都通用的方法

第八條:覆蓋equals是請遵守通用約定

滿足下列四個條件之一,就不需要覆蓋equals方法:

  1. 類的每個例項本質上都已唯一的。不包括代表值的類,如:Integer,String等,Object提供的equals方法就夠用了

  2. 不關心是否提供了“邏輯相等”的測試功能。對於Random類,使用者只關心函式返回的隨機數,不會關心產生的兩個隨機數是不是相等,所以對其進行equal方法覆蓋將沒有意義

  3. 超類已經覆蓋了equals,從超類繼承過來的行為對於子類也是合適的。

  4. 類是私有的或是包級私有的,並且確定它的equals方法永遠不會被呼叫。同時為了防止該equals被呼叫,可以如此覆蓋:

    @Override
    public boolean equals(Object obj) {
    throw new AssertionError();
    }

    注:此處原文是 類是私有的或包級私有的,可以確定它的equals方法永遠不會被呼叫 。但翻譯的不太合適,原文為

    The class is private or package-private, and you are certain that its equals method will never be invoked.

如果類有自己的“邏輯相等”的概念,通常屬於“值類”的情形,且超類還沒有覆蓋equals以實現期望的行為,此時就需要對euqals進行覆蓋。但對於“每個值最多隻存在一個物件”的類即單例模式實現的類則不需要覆蓋。

在覆蓋equals方法時,需要遵守其通用約定:

  1. 自反性 。對於任何非null的引用值x,x.equals(x)必須==true。

  2. 對稱性 。對於任何非null的引用值x和y,當且僅當y.equals(x)==true時,x.equals(y)必須==true。

    class CaseInsensitiveString {
       private final String s;
    
       public CaseInsensitiveString(String s) {
           if (s == null){
               throw new NullPointerException();
           }
           this
    .s = s; } //違反了對稱性 //企圖與與普通的String物件進行互操作,但是String類中的equals方法並不知道這個類 //一旦違反了對稱性,當其他物件面對你的物件時,行為是無法知道的 @Override public boolean equals(Object obj) { if (obj instanceof CaseInsensitiveString){ return s.equalgnoreCase(((CaseInsensitiveString) obj).s); } if (obj instanceof String){ return s.equalsIgnoreCase((String) obj); } return false; } }
  3. 傳遞性 。對於任何非null的引用值x, y和z,如果x.equals(y)==true,並且y.equals(z)==true, 那麼對於x.equals(z)也必須==true。

  4. 一致性 。對於任何非null的引用值x和y,只要equals的比較操作在物件中所用的資訊沒有被修改,多次呼叫x.equals(y)就會一致地返回同樣的結果。

    可變物件在不同的時候可以與不同的物件相等,而不可變物件則不會這樣。無論類是否不可變,都不要使用equals方法依賴於不可靠的資源。

  5. 對於任何非null的引用值x,x.equals(null)必須==false。

實現高質量equals方法的訣竅,下列每一項都是基於前一項:

  1. 使用==操作符檢查“引數是否為這個物件的引用”。效能優化

  2. 使用instanceof操作符檢查“引數是否為正確的型別”

  3. 把引數轉換成正確的型別

  4. 對於該類中的每個“關鍵”域,檢查引數中的域是否對該物件中對應的域相匹配

    1. 對於float,使用Float.compare;對於double,使用Double.compare

    2. 有些物件引用域包含null可能是合法的,使用下面的方式避免NullPointException

      (field == null ? o.field == null : field.equals(o.field))
    3. 域的比較可能會影響到equals效能。應先比較最有可能不一致的域,或是開銷最低的域

  5. 當你編寫完成了equals方法後,問三個問題:是否是對稱的、傳遞的、一致的。同時還需編寫測試單元來檢驗。另外兩個特性通常會自動滿足。

最後的告誡:

  1. 覆蓋equals時總要覆蓋覆蓋hashCode
  2. 不要企圖讓equals方法過於智慧
  3. 不要將equals宣告中的Object物件替換為其他的型別

第九條:覆蓋equals時總要覆蓋hashCode

在覆蓋equals方法時,如果不覆蓋hashCode方法,會導致該類無法與HashMap、HashSet和HashTable一起正常運作。相等的物件必須具有相等的雜湊碼。

class PhoneNumber {
    private final short areaCode;
    private final short prefix;
    private final short lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        rangeCheck(areaCode, 999, "area code");
        rangeCheck(prefix, 999, "prefix");
        rangeCheck(lineNumber, 999, "line number");
        this.areaCode = (short) areaCode;
        this.prefix = (short) prefix;
        this.lineNumber = (short) lineNumber;
    }

    private static void rangeCheck(int arg, int max, String name) {
        if (arg < 0 || arg > max){
            throw new IllegalArgumentException(name + ":" + arg);
        }
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this){ //使用 == 操作符檢查 “引數是否為這個物件的引用”
            return true;
        }
        if (!(obj instanceof PhoneNumber)){//使用instanceof檢查“引數是否為正確的型別”
            return false;
        }
        PhoneNumber pn = (PhoneNumber) obj;//轉換為正確的型別
        //為了獲得最佳效能,先比較最有可能不一樣的域
        return pn.lineNumber == lineNumber
                && pn.prefix == prefix
                && pn.areaCode == areaCode;
    }
}

在執行下列操作時

Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5360), "jenny");

//執行下列操作時,雖然已經重寫這個值類的equals方法
//但由於前後兩個物件的hashCode值不同,所以不會執行我們預期的操作
m.get(new PhoneNumber(707, 867, 5360));

一個好的雜湊函式通常傾向於“為不想等的物件產生不想等的雜湊碼”。下面是一種簡單的解決辦法:

  1. 把某個非零的常數值,比如說17,儲存在一個名為result的int型別的變數中

  2. 對於物件中每個關鍵域 f (知equals方法中涉及的每個域),完成以下步驟:

    1. 為該域計算int型別的雜湊碼c:

      1. 如果該域是boolean型別,則計算( f ? 1 : 0)
      2. 如果該域是byte、char、short或者int型別,計算(int)f
      3. 如果該域是long型別,則計算(int)(f ^ (f >>> 32))
      4. 如果該域是float型別,則計算Float.floatToIntBits(f)
      5. 如果該域是double型別,則計算Double.doubleToLongBits(f),然後按照2計算
      6. 如果該域是一個物件引用,並且該類的equals方法通過遞迴地呼叫equals的方式來比較這個域,則同樣為這個域遞迴地呼叫hashCode。如果需要更復雜的比較,則為這個域計算一個“正規化”,然後針對這個正規化呼叫hashCode。如果這個域的值為null,則返回0(或者其他某個常熟,但通常是0)
      7. 如果該域是一個數組,則要把每一個元素當作單獨的域來處理。
    2. 按照下面額公式,把上面步驟中計算的雜湊碼c合併到result中:

      result = 31 * result + c;
    3. 返回result

    4. 編寫單元測試來檢驗

在雜湊碼的計算過程中。可以把冗餘域(該域的值可以通過其他域值計算出來)排除在外。同時必須排除equals比較中沒有用到的域。

對於PhoneNumber類,可以這樣覆蓋其hashCode方法

@override
public int hashCode() {
    int result = 17;
    result = 31 * result + areaCode;
    result = 31 * result + prefix;
    result = 31 * result + lineNumber;
    return result;
}

如果一個類是不可變的,並且計算雜湊碼的開銷也比較大,可以考慮把雜湊碼快取在物件內部。如果這種型別的大多數物件會被用作雜湊鍵,就應該在建立例項的時候計算雜湊碼,否則,可以選擇直到hashCode被第一次呼叫的時候才初始化。

不要試圖從雜湊碼計算中排除掉一個物件的關鍵部分來提高效能。

第十條:始終要覆蓋toString

建議所有的子類都覆蓋這個方法。

在實際應用中,toString方法應該返回物件中包含的所有值得關注的資訊。同時決定是否在文件中指定返回值的格式,對於值類,建議這麼做,同時再提供一個相匹配的靜態工廠或者構造器,以便於程式設計師可以很容易的在物件和它的字串表示法之間轉換,例如:BigInteger、BigDecimal和絕大多數的基本型別包裝類。

但不足之處在於,如果該類被廣泛使用,一旦指定格式,即必須始終堅持這種格式,如果在將來的發行版本中改變,就會破壞程式碼和資料。如果不指定格式,就可以保留靈活性,便於在將來的發行版本中增加資訊,或者改進格式。

無論是否指定格式,都應在文件中明確地表明你的意圖。同時都為toString返回值中包含的所有資訊,提供一種程式設計式的訪問途徑。

第十一條:謹慎地覆蓋clone

Cloneable介面中沒有clone方法,Object的clone方法是protected的。Cloneable決定了Object中受保護的clone方法的實現行為:如果一個類實現了Cloneable,Object的clone方法就返回該物件的逐域拷貝,否則就會丟擲CloneNotSupportException。

在克隆物件時,如果每個域包含一個基本型別的值,或者包含一個指向不可變物件的引用,那麼不需要再做進一步處理。如果物件中包含的域引用了可變的物件,此時就需要使用深拷貝。

實際上,clone方法就是另一個構造器,你必須確保它不會傷害到原始的物件,並確保正確地建立被克隆物件中的約束條件。clone架構與引用可變物件的final域的正常用法是不相相容的,因為一個域被final修飾後,就無法再呼叫clone方法對其進行克隆(即賦值)。

克隆複雜物件最後一種辦法是先呼叫super.clone,然後把結果中的所有域都設定成他們的空白狀態,然後呼叫高層的方法來重新產生物件的狀態。

覆蓋版本的clone方法如果是公有的,就應該將Object中的clone丟擲的CloneNotSupportException進行try-catch處理,因為這樣會使得覆蓋版本中的clone使用起來更加輕鬆。如果專門為了繼承而設計的類,就應該模擬Object.clone的行為,這樣使得子類具有實現或不實現Cloneable介面的自由。

如果用執行緒安全的類實現Cloneable介面,要使得clone方法也有很好的同步。

另一個實現物件拷貝的好辦法是提供一個拷貝構造器,或者拷貝工廠,比起Cloneable/clone有以下優勢:

  • 不依賴域某一種很有風險、語言之外的物件建立機制
  • 不要求遵守尚未指定好文件的規範
  • 不會與final域的正常使用發生衝突
  • 不會丟擲不必要的手賤異常
  • 不需要進行型別轉換
  • 可以帶一個引數,引數型別是通過該類實現的介面。假設你有一個HashSet,並且希望把他拷貝成一個TreeSet,使用轉換構造器:new TreeSet(s)

第十二條:考慮實現Comparable介面

類實現該介面,就表明它的例項具有內在的排序關係,可以跟許多泛型演算法以及依賴於該介面的集合實現進行協作,java平臺類庫中的所有值類都實現了該藉口。

將這個物件與指定物件進行比較。當該物件小於、等於或大於指定物件的時候,分別返回一個負整數、零或正整數。若由於指定物件型別無法比較,則丟擲ClassCastException。

說明(sgn為符號函式):

  • 必須確保所有的x和y都滿足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))(也暗示著,當且僅當y.compareTo(x)丟擲異常時,x.compareTo(y)才必須丟擲異常)
  • 必須確保此關係可傳遞
  • 必須確保x.compareTo(y) == 0 暗示著所有的z都滿足sgn(x.compareTo(z)) == sgn(y.compareTo(z))
  • 強烈建議(x.compareTo(y) == 0) == (x.equals(y)),但並非絕對必要。一般來說,任何實現了Comparable介面的類,若違反了這個條件,都應明確予以說明。推薦使用的說法:“注意:該類具有內在排序功能,但與equals不一致”

    違反compareTo約定的類也會破壞其他依賴於比較關係的類,例如TreeSet和TreeMap,以及Collections和Array。

告誡:無法在用新的值組建擴充套件可例項化的類時的同時保持compareTo約定,除非願意放棄面向物件的抽象優勢。 如果想為一個實現了Comparable介面的類增加值組建,要編寫一個不相關的類,其中包含第一個類的一個例項。

如果遵守上述“說明”中的最後一條,那麼由compareTo所施加的順序關係就被認為“與equals一致”,如果違反這條規則,就是“與equals不一致”,如果不一致,仍然能正常工作,但如果一個有序集合包含了該類元素,該集合可能就無法遵守相應集合介面(Collection, Set, Map)的通用約定。因為,這些介面的通用約定是按照equals來定義的,但是有序集合使用了由compareTo來定義。例如,new BigDecimal(“1.0”)和new BigDecimal(“1.00”)在HashSet中會兩個都存在,但是在TreeSet中只存在一個。

Comparable介面是引數化的,而且comparable方法是靜態的型別,不必進行型別檢查,也不必對它的引數進行型別轉換。如果引數不合適,甚至無法編譯。

如果一個類用多個關鍵域,那麼必須從最關鍵的域開始,逐步進行到所有的重要域。如果某個域的比較產生了非零的結果,則整個比較結束。

public int compareTo(PhoneNumber pn){
        if (areaCode < pn.areaCode)
            return -1;
        if (areaCode > pn.areaCode)
            return 1;
        // area code are equals, compare prefixes
        if (prefix < pn.prefix)
            return -1;
        if (prefix > pn.prefix)
            return 1;
        //area code and prefixes are equals, compare line number
        if (lineNumber < pn.lineNumber)
            return -1;
        if (lineNumber > pn.lineNumber)
            return 1;
        //all fields are equals
        return 0;
    }

如果compareTo的約定沒有指定返回值的大小,而只是指定了返回值的符號,可以對上述程式碼進行簡化

    public int compareTo(PhoneNumber pn){
        int areaCodeDiff = areaCode - pn.areaCode;
        if (areaCodeDiff != 0)
            return areaCodeDiff;

        int prefixDiff = prefix - pn.prefix;
        if (prefixDiff != 0)
            return prefixDiff;

        return lineNumber - pn.lineNumber;
    }

但這種簡化方法,除非確定相關的域不會為負值,或者更一般的情況:最小和最大的可能域值之差小於或者等於INTEGER.MAX_VALUE。