1. 程式人生 > >【讀書筆記】《Effective Java》(2)--對於所有物件都通用的方法

【讀書筆記】《Effective Java》(2)--對於所有物件都通用的方法

又讀了一章,之前一直覺得Java的體系很完善了,讀了這一章,發現原來Java平臺本身也有一些設計不周到的地方,而且有些地方因為已經成為公開API的一部分還不好改,相信繼續讀下去對Java的瞭解會更深一步的。

昨天下載了VS Code,嘗試了一下,感覺比sublime還要用一些,尤其Markdown支援預覽,不用設定,正好我覺得在oneNote上做筆記格式不好控制,就學了下Markdown的語法,用md寫下了這一章的筆記…

對於所有物件都通用的方法

8.覆蓋equals 方法時請遵守通用約定

  • 不需要覆蓋equals的情況:

    1. 類的每個例項本質上都是唯一的。
    2. 不關心類是否提供了“邏輯相等”的測試功能。
    3. 超類已經覆蓋了equals方法,且從超類繼承過來的行為對於子類來說也合適。
    4. 類是私有的或者包級私有的,可以確定他的equals方法永遠不會被呼叫
  • equals方法實現了等價關係(和離散數學裡的一樣),也就是:

    1. 自反性:

    自身相等;

  • 對稱性:
    x和y,有x.equals(y),就有y.equals(x);
  • 傳遞性:
    有x、y、z,當x.equals(y)、y.equals(z),就有x.equals(z);
  • 一致性:
    多次呼叫equals方法,結果一樣;
  • 非空性(這一條不是離散裡的):
    x.equals(null)結果為false;
  • equals方法要注意的地方:

    1. 對於傳遞性:我們無法在擴充套件可例項化的類的同時,既增加新的值元件,同時又保留equals約定

      對於這種情況,有一種做法是將equals方法中檢測Object是否為類的例項的方法:

      if(o instaceof MyClass){…};

      換成另外一種:

      if(o==null|o.getClass()!=getClass()){…}

      但是這種方法並不好,根據 里氏替換原則 ,一個型別的任何重要屬性也要適用於它的子類,而這種方法會導致equals中傳入父類無法被識別,
      一個好的做法是使用複合替代繼承,這個方法將在之後提到。

      • 一個Java沒做好的地方:java.sql.Timestamp類對java.util.Date類進行了擴充套件,但這兩個類的equals方法違反了對稱性
    2. 對於一致性:不要將equals方法依賴於不可靠的資源,確保當類本身不變時,多次呼叫equals方法返回的結果一致。
      • 一個Java沒做好的地方:java.net,URL的equals方法依賴於主機的IP地址,而同一個主機名多個IP也是常見的,這導致了一些問題。
    3. 對於非空性:對於equals方法傳入的Object,需要檢驗是否為空,當時用instanceof時,這是不需要的,因為instanceof null 返回false,但是對於上述出現的使用getClass方法,這是需要的,因為null.getClass()將報錯。
  • 綜上,實現高質量equals方法的要點:

    1. 使用==操作符檢測,檢驗是否是同一個物件的應用,這對於重型物件的比較來說有助於提高效能;
    2. 使用intanceof方法操作符檢測“引數是否為正確的型別”,這個操作符對於實現介面的類、父類都返回true;
    3. 把引數轉化成正確的類
    4. 對於該類的每個 “關鍵域”,檢查引數中的域是否與該物件對應的域相匹配

      • 對於既不是float和double的基本型別,可以使用==直接比較
      • 對於float和double基本型別,因為存在比較精度的問題,使用Float.compare()和Double.compare()方法
      • 對於引用型別,遞迴呼叫equals方法
      • 對於允許為空的引用型別域,為了避免丟擲空指標異常,可以使用如下格式:

      (field==null?o.field==null:field.equals(o.field))
      或者(field==o.field||(field!=null&&field.equals(o.field))),這種形式對於相同的物件引用更快一些

  • 編寫完equals方法,想一想是否滿足自反性、對稱性、傳遞性、一致性,並且單元測試

  • 覆蓋equals方法總是同時覆蓋hashCode方法(下一條詳述)
  • 不要企圖讓equals方法過於智慧,僅僅負責簡單檢測的equals方法易於編寫,但是考慮太多例外情況會使程式碼過於複雜
  • 不要將equals方法傳入的引數更換成其他類,始終將其保持為Object,否則將不會覆蓋超類的方法,這種情況屬於過載了equals方法,一個方便的做法是使用@override註解檢測
  • 9.覆蓋equal方法時總是覆蓋hashCode方法

    • 為什麼:為了遵循hashCode的通用規定:

      1. 只要equals方法用到的資訊沒有改變,hashCode多次呼叫返回的值應當相同
      2. equals一致的物件需要返回相同的hashCode
      3. 不同的物件則不要求返回相同的hashCode,但返回不同的hashCode有助於提高散列表的效能
    • 如何編寫一個好的hashCode:

      1. 首先,選中某一個非零的常數,儲存為result(通常選擇質奇數,尤其看好31,因為31有一個很好的特性,對31的乘法可以通過移位和減法完成[31*i==(i<<5)-i])
      2. 對於物件的每一個關鍵域,做如下操作:

        a. 為該域計算int型別的雜湊值c:

        • 如果是bool,計算f?1:0;
        • 如果是byte、char、short、int,則計算(int)f;
        • 如果是long,計算(int)(f^(f>>>32))
        • 如果是float,計算Float.floatToIntBits(f)
        • 如果是Double,計算Double.doubleToLongBits(f),然後再按long的規則轉化
        • 如果是域是一個物件,並且物件的equals方法對於這個物件域遞迴呼叫equals,則hashCode方法同樣遞迴呼叫這個物件域的hashCode,
          如果物件域的比較很複雜,可以設計一個正規化(規定一個計算方式),按計算公式返回hashCode
        • 如果物件是一個數組。對陣列中的每一個元素求hashCode,在Java1.5以後,Array有一個方法Array.hashCode,可以使用

        b. 將上述計算的int值按照如下公式相加合併:

        • result=31*result+c;
      3. 返回result;
      4. 測試
    • 注意點:

      1. 如果一個域的值可以由其他關鍵域求出,則這個域可以被排除在外
      2. equals中沒有用到的域儘量不要出現在hashCode的計算中,著很有可能違反“equals方法一致,hashCode方法的值也要一致的約定”
    • 兩個不好的行為:

      1. 儲存hashCode的確切值用於其他函式中,或者計算中,因為hashCode的產生收到計算函式的影響,可能會出現更改計算函式內部實現的情況,這樣的話hashCode的確切值就會改變,依賴於他們(確切hashCode值)的函式就會出問題
      2. 試圖排除一個物件的關鍵部分來提高雜湊計算的效能,這會導致雜湊的效能(分散的能力)下降,導致雜湊線性的查詢效能變成平方級別,這個行為曾在Java1.2的字串中出現過

      為了提高計算雜湊碼的效能,可以考慮將雜湊碼快取到物件內部,以及延遲計算的方法

    10.始終要覆蓋toString

    • 原因:物件使用起來更加清晰,除錯起來更方便

    • 注意點:

      1. 預設toString 方法返回的是@+雜湊碼的無符號16進製表示
      2. toString應當包含一個物件的所有值得關注的資訊,如果物件太大或者狀態資訊不好用字串表示,這時候應該返回摘要資訊
      3. 注意頭String的格式,並將它寫入文件

        因為toString是公開的,也就是說,可能有客戶程式碼使用toString做持久化或者其他依賴於toString格式的用途,這個時候,一個清晰、永不更改的格式是尤為重要的,如果不能提供一個穩定不變的格式,也要寫入文件提醒

      4. 最好配套一個靜態工廠方法,用於將物件和物件的toString返回的字串之間相互轉化

    11.謹慎地覆蓋clone (看完之後的想法是個人絕不建議使用clone,對它無視掉最好

    • 背景知識:

      1. clone方法是Object的保護方法,當類沒有實現Cloneable介面時,呼叫丟擲CloneNotSupportedException異常,實現介面的話,Object的子類把clone重寫為public的方法,方可呼叫(書上的意思是:也需要通過反射機制才有可能成功呼叫clone方法,我想這是指內部呼叫細節或者特指Object的呼叫,其他類的clone是public的,從表面看不需要反射)
      2. 預設的約定是不在clone方法中呼叫構造器,但是這個約定不太好遵守,因為clone的行為就像是一個構造器一樣,似乎完全可以呼叫構造器來幫助完成任務
      3. 對於繼承的類,clone方法有類似於構造器呼叫鏈的機制,即子類clone方法呼叫super.clone,一直呼叫到Object的clone
    • clone方法需要遵守的通用約定:

      1. x.clone()!=x 結果為true
      2. x.clone().getClass()==x.getClass()為true
      3. x.clone().equals(x)結果為true
    • 雷區:

      1. 如果子類的父類沒有按規定實現一個適合的clone方法,子類的clone方法必然會失敗,比如父類的clone方法中呼叫構造器,這回返回一個錯誤的類
      2. 如果類的物件域涉及到深拷貝需要特別注意,因為clone返回的是一個==操作返回false,而equals方法返回true的物件,這意味著記憶體空間不能共享
      3. 在深拷貝過程中,如果有域是final的話就麻煩了,會有編譯錯誤“cannot be assigned”,因為clone方法禁止給final的域賦值,這個時候可能需要考慮去掉final關鍵字
      4. 和構造器一樣,clone原則上不應該呼叫任何非final的方法,因為非final方法可能被子類重寫實現,這會導致clone的不確定性
      5. 執行緒安全的類,clone方法需要客戶程式碼自己同步
    • 最後的建議:使用拷貝構造器或者拷貝工廠而不是clone方法會是個好選擇。

    12. 考慮實現Comparable介面

    • 優點:實現後在配合泛型演算法以及基於它們的集合過程中表現好

    • 注意點:

      1. 實現一個好的Comparable介面需要遵守和equals相同的約定,缺點也是相同的:我們無法在擴充套件可例項化的類的同時,既增加新的值元件,同時又保留compareTo的約定
      2. 同時,最好等同性測試compareTo和equals方法的結果相同,不同的話並不會導致災難性的影響,不過會有些怪異。要知道有序集合通過compareTo檢測等同性,而一般集合使用equals
      3. compareTo是順序比較,所以比較的時候要注意比較順序,從一個類的最關鍵的域開始,逐個比較到不重要的域
      4. 如果通過減法實現比較,返回結果的符號,需要注意結果的範圍是否會超過int的最大值,這種問題比較難以發現