【讀書筆記】《Effective Java》(2)--對於所有物件都通用的方法
又讀了一章,之前一直覺得Java的體系很完善了,讀了這一章,發現原來Java平臺本身也有一些設計不周到的地方,而且有些地方因為已經成為公開API的一部分還不好改,相信繼續讀下去對Java的瞭解會更深一步的。
昨天下載了VS Code,嘗試了一下,感覺比sublime還要用一些,尤其Markdown支援預覽,不用設定,正好我覺得在oneNote上做筆記格式不好控制,就學了下Markdown的語法,用md寫下了這一章的筆記…
對於所有物件都通用的方法
8.覆蓋equals 方法時請遵守通用約定
不需要覆蓋equals的情況:
- 類的每個例項本質上都是唯一的。
- 不關心類是否提供了“邏輯相等”的測試功能。
- 超類已經覆蓋了equals方法,且從超類繼承過來的行為對於子類來說也合適。
- 類是私有的或者包級私有的,可以確定他的equals方法永遠不會被呼叫
equals方法實現了等價關係(和離散數學裡的一樣),也就是:
- 自反性:
自身相等;
- 自反性:
- 對稱性:
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方法要注意的地方:
對於傳遞性:我們無法在擴充套件可例項化的類的同時,既增加新的值元件,同時又保留equals約定
對於這種情況,有一種做法是將equals方法中檢測Object是否為類的例項的方法:
if(o instaceof MyClass){…};
換成另外一種:
if(o==null|o.getClass()!=getClass()){…}
但是這種方法並不好,根據 里氏替換原則 ,一個型別的任何重要屬性也要適用於它的子類,而這種方法會導致equals中傳入父類無法被識別,
一個好的做法是使用複合替代繼承,這個方法將在之後提到。- 一個Java沒做好的地方:java.sql.Timestamp類對java.util.Date類進行了擴充套件,但這兩個類的equals方法違反了對稱性
- 對於一致性:不要將equals方法依賴於不可靠的資源,確保當類本身不變時,多次呼叫equals方法返回的結果一致。
- 一個Java沒做好的地方:java.net,URL的equals方法依賴於主機的IP地址,而同一個主機名多個IP也是常見的,這導致了一些問題。
- 對於非空性:對於equals方法傳入的Object,需要檢驗是否為空,當時用instanceof時,這是不需要的,因為instanceof null 返回false,但是對於上述出現的使用getClass方法,這是需要的,因為null.getClass()將報錯。
綜上,實現高質量equals方法的要點:
- 使用==操作符檢測,檢驗是否是同一個物件的應用,這對於重型物件的比較來說有助於提高效能;
- 使用intanceof方法操作符檢測“引數是否為正確的型別”,這個操作符對於實現介面的類、父類都返回true;
- 把引數轉化成正確的類
對於該類的每個 “關鍵域”,檢查引數中的域是否與該物件對應的域相匹配
- 對於既不是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方法,想一想是否滿足自反性、對稱性、傳遞性、一致性,並且單元測試
9.覆蓋equal方法時總是覆蓋hashCode方法
為什麼:為了遵循hashCode的通用規定:
- 只要equals方法用到的資訊沒有改變,hashCode多次呼叫返回的值應當相同
- equals一致的物件需要返回相同的hashCode
- 不同的物件則不要求返回相同的hashCode,但返回不同的hashCode有助於提高散列表的效能
如何編寫一個好的hashCode:
- 首先,選中某一個非零的常數,儲存為result(通常選擇質奇數,尤其看好31,因為31有一個很好的特性,對31的乘法可以通過移位和減法完成[31*i==(i<<5)-i])
對於物件的每一個關鍵域,做如下操作:
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;
- 返回result;
- 測試
注意點:
- 如果一個域的值可以由其他關鍵域求出,則這個域可以被排除在外
- equals中沒有用到的域儘量不要出現在hashCode的計算中,著很有可能違反“equals方法一致,hashCode方法的值也要一致的約定”
兩個不好的行為:
- 儲存hashCode的確切值用於其他函式中,或者計算中,因為hashCode的產生收到計算函式的影響,可能會出現更改計算函式內部實現的情況,這樣的話hashCode的確切值就會改變,依賴於他們(確切hashCode值)的函式就會出問題
- 試圖排除一個物件的關鍵部分來提高雜湊計算的效能,這會導致雜湊的效能(分散的能力)下降,導致雜湊線性的查詢效能變成平方級別,這個行為曾在Java1.2的字串中出現過
為了提高計算雜湊碼的效能,可以考慮將雜湊碼快取到物件內部,以及延遲計算的方法
10.始終要覆蓋toString
原因:物件使用起來更加清晰,除錯起來更方便
注意點:
- 預設toString 方法返回的是@+雜湊碼的無符號16進製表示
- toString應當包含一個物件的所有值得關注的資訊,如果物件太大或者狀態資訊不好用字串表示,這時候應該返回摘要資訊
注意頭String的格式,並將它寫入文件
因為toString是公開的,也就是說,可能有客戶程式碼使用toString做持久化或者其他依賴於toString格式的用途,這個時候,一個清晰、永不更改的格式是尤為重要的,如果不能提供一個穩定不變的格式,也要寫入文件提醒
最好配套一個靜態工廠方法,用於將物件和物件的toString返回的字串之間相互轉化
11.謹慎地覆蓋clone (看完之後的想法是個人絕不建議使用clone,對它無視掉最好
背景知識:
- clone方法是Object的保護方法,當類沒有實現Cloneable介面時,呼叫丟擲CloneNotSupportedException異常,實現介面的話,Object的子類把clone重寫為public的方法,方可呼叫(書上的意思是:也需要通過反射機制才有可能成功呼叫clone方法,我想這是指內部呼叫細節或者特指Object的呼叫,其他類的clone是public的,從表面看不需要反射)
- 預設的約定是不在clone方法中呼叫構造器,但是這個約定不太好遵守,因為clone的行為就像是一個構造器一樣,似乎完全可以呼叫構造器來幫助完成任務
- 對於繼承的類,clone方法有類似於構造器呼叫鏈的機制,即子類clone方法呼叫super.clone,一直呼叫到Object的clone
clone方法需要遵守的通用約定:
- x.clone()!=x 結果為true
- x.clone().getClass()==x.getClass()為true
- x.clone().equals(x)結果為true
雷區:
- 如果子類的父類沒有按規定實現一個適合的clone方法,子類的clone方法必然會失敗,比如父類的clone方法中呼叫構造器,這回返回一個錯誤的類
- 如果類的物件域涉及到深拷貝需要特別注意,因為clone返回的是一個==操作返回false,而equals方法返回true的物件,這意味著記憶體空間不能共享
- 在深拷貝過程中,如果有域是final的話就麻煩了,會有編譯錯誤“cannot be assigned”,因為clone方法禁止給final的域賦值,這個時候可能需要考慮去掉final關鍵字
- 和構造器一樣,clone原則上不應該呼叫任何非final的方法,因為非final方法可能被子類重寫實現,這會導致clone的不確定性
- 執行緒安全的類,clone方法需要客戶程式碼自己同步
最後的建議:使用拷貝構造器或者拷貝工廠而不是clone方法會是個好選擇。
12. 考慮實現Comparable介面
優點:實現後在配合泛型演算法以及基於它們的集合過程中表現好
注意點:
- 實現一個好的Comparable介面需要遵守和equals相同的約定,缺點也是相同的:我們無法在擴充套件可例項化的類的同時,既增加新的值元件,同時又保留compareTo的約定
- 同時,最好等同性測試compareTo和equals方法的結果相同,不同的話並不會導致災難性的影響,不過會有些怪異。要知道有序集合通過compareTo檢測等同性,而一般集合使用equals
- compareTo是順序比較,所以比較的時候要注意比較順序,從一個類的最關鍵的域開始,逐個比較到不重要的域
- 如果通過減法實現比較,返回結果的符號,需要注意結果的範圍是否會超過int的最大值,這種問題比較難以發現