天天看點

【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, , "area code");
        rangeCheck(prefix, , "prefix");
        rangeCheck(lineNumber, , "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 <  || 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(, , ), "jenny");

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

一個好的散列函數通常傾向于“為不想等的對象産生不想等的散列碼”。下面是一種簡單的解決辦法:

  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 =  * result + c;
                 
    3. 傳回result
    4. 編寫單元測試來檢驗

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

對于PhoneNumber類,可以這樣覆寫其hashCode方法

@override
public int hashCode() {
    int result = ;
    result =  * result + areaCode;
    result =  * result + prefix;
    result =  * 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 -;
        if (areaCode > pn.areaCode)
            return ;
        // area code are equals, compare prefixes
        if (prefix < pn.prefix)
            return -;
        if (prefix > pn.prefix)
            return ;
        //area code and prefixes are equals, compare line number
        if (lineNumber < pn.lineNumber)
            return -;
        if (lineNumber > pn.lineNumber)
            return ;
        //all fields are equals
        return ;
    }
           

如果compareTo的約定沒有指定傳回值的大小,而隻是指定了傳回值的符号,可以對上述代碼進行簡化

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

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

        return lineNumber - pn.lineNumber;
    }
           

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