1. 程式人生 > >《Effective Java》第3章 對於所有物件都通用的方法

《Effective Java》第3章 對於所有物件都通用的方法

Object類預設為所有類的基類,其雖然為一個具體的類,但是它的設計主要是為了擴充套件,而它的所有非final的方法(equals, hashCode, toString, clone和finalize)都有明確的通用約定, 如果在自定義類時要重寫這些方法,需要注意主動遵守這些約定,不然一些依賴於這些約定的類(如HashMap和HashSet)將無法正確工作。

通用概念

值類:表示值的類,也就是說這個類的物件的相等是說其代表的值是一樣的,並非僅僅物件的相等

1.覆蓋equals方法時切記遵守通用約定【Item 8】

1)避免問題發生的最好的方式是不覆蓋,那麼如下情況可以不用覆蓋equals方法

a)類的每個例項本質上都是唯一的,Object類的equals方法實現對於這些類來說是正確的行為

b)不關心類是否提供了“邏輯相等”的測試功能,也就是說在這個類在使用過程中並不關心兩個物件的值在邏輯上相等的功能

c)父類已經覆蓋了equals方法,從父類繼承過來的行為對於子類也是合適的。例如大多數Set/List/Map均從AbstractSet/AbstractLst/AbstractMap繼承equals的實現

d)值類中的設計為單例的類,以及列舉型別

2)equals方法實現了等價關係,因此其在被覆蓋是需要保持等價的特性,數學中等價需要滿足如下條件:

a)自反性,對於任何非null的值x,x.equals(x)返回必須為true

b)對稱性,x.equals(y)返回true,則y.equals(x)也必須為true,這依賴於兩個物件對應類的equals方法的實現,雙方相等的口徑需要是一致的

c)傳遞性,x.equals(y)返回true,y.equals(z)返回為true, 則x.equals(z)也必須為true

d)一致性,只要equals中用來比較的值沒有改變,多次呼叫equals方法返回值應該一致的,避免使equals方法依賴於不可靠的資源

e)對於任何非null的引用值x,x.equals(null)必須返回false

3)高質量的equals方法實現訣竅

a)使用==操作符檢查“引數是否這個物件的引用”,若是,直接返回true

b)使用instanceof操作符檢查“引數是否為正確的型別”,可以是本型別或者實現統一介面的型別

c)把引數轉換為正確的型別,由於前一步已經進行了型別判定,因此此處可以直接進行型別轉換

d)對於該類中的每個“關鍵”域,檢查引數物件中的域是否與該物件中對應的域相匹配,這裡可以從類中最關鍵的域開始檢查,一旦不等直接返回false,對於域值的判定方式分別為:基本型別(float,double除外)使用==操作符進行比較;對於物件引用域可以通過遞迴呼叫equals方法進行;對於float使用Float.compare,對於double使用Double.compare;陣列域按照上述比較規則應用於每一個元素,如果陣列上每個元素都很重要,可以使用1.5版本新加的Arrays.equals方法

對於域值的比較,為了效能考慮,建議先比較最可能不一致的域,儘可能減少不必要code的執行

4)零碎的注意點

a)我們無法在擴充套件可例項化的類的同時,既增加新的值元件,同時又保留equals約定,這個時候可以通過組合來代替繼承。

b)可以在一個抽象類的子類中增加新的值元件,而不違反equals約定

c)在重寫完equals方法之後一定要針對自反性,對稱性,傳遞性和一致性寫自測code進行驗證,如果不滿足要及時調整,以免為debug增加不必要的負擔

d)重寫equals方法時總要重寫hashCode方法

e)不要企圖equals方法過於智慧,如果將該方法設計得過於智慧,可能增加不必要的負擔,由於JDK中類好多功能依賴equals,重寫的越複雜程式碼越難維護

f)不要將equals宣告中的Object物件替換為其它型別,這點可以通過@Override註釋來幫忙保證,因為重寫需要成員方法的簽名一致(方法名與引數唯一確認簽名)

2.重寫equals方法時一定記得要重寫hashCode方法【Item 9】

1)相等的物件必須有相等的雜湊碼(hash code)

2)一個好的雜湊函式通常傾向於“為不相等的物件產生不相等的雜湊碼”

3)雜湊碼的一種計算方式是,通過物件的每個域值做相應的數學計算(加減乘除),最後所有計算結果加起來構成該物件的雜湊碼,通常來講null的雜湊碼為0

4)雜湊碼的計算需要排除冗餘域(可以通過其它域計算出來的域),以及equals方法中比較沒有用到的域

5)如果一個類是不可變的,並且計算雜湊碼的開銷比較大,那麼建議將雜湊碼保存於物件內部,不是每次請求都計算一遍

6)如果你覺得型別的大多數物件會被用作雜湊鍵值,那麼需要物件被建立時計算雜湊碼,如果不是,則可以延遲hash code的計算到hashCode方法被第一次呼叫的時候

7)不要嘗試從雜湊碼的計算中排除掉一個物件的關鍵部分來提高效能,否則會本末導致,犧牲程式執行的正確性換來的效能沒有任何意義

3.始終要重寫toString方法【Item 10】

1)建議所有子類都重寫toString方法,toString應該返回物件中包含的所有值得關注的資訊

2)toString是debug時獲取資訊的重要方式,好的toString方法能夠使得除錯工作事半功倍

3)toString方法可以採用固定格式的實現方式也可以不固定而是,但是一定注意要註釋清楚輸出格式對應的意義

4)無論是否指定格式,都為toString返回值中包含的所有資訊,提供一種程式設計式的訪問途徑,這樣有需求要獲取輸出資訊中的內容時可以通過getXXX或者物件公開域來獲取,不用費勁去解析toString返回內容

4.謹慎的重寫clone方法【Item 11】

1)永遠不要讓客戶去做任何類庫能夠為客戶完成的事情,在子類的clone方法中呼叫super.clone,父類會做一部分的轉換工作,不需要子類來進行

2)clone就是另一個構造器,你必須確保它不會傷害到原始物件,並確保正確的建立被克隆物件中的約束條件

3)clone架構與應用可變物件的final域的正常用法是不相容的,除非在原始物件和克隆物件之間共享該可變物件,否則想要保持類的可克隆性,需要去掉某些域的final屬性

4)對於不可變類,支援物件的拷貝並沒有太大的意義,因為被拷貝的物件與原始物件病沒有實質的不同

5)在實現clone的時候一定要拷貝的深度,以及拷貝的同步性控制,以及原始物件與拷貝物件相互之間的影響

5.考慮實現Comparable介面【Item 12】

1)類實現了Comparable介面,就表明它的例項具有內在的排序關係

2)只要類實現了Comparable介面,JDK類庫中依賴該介面的諸多功能你都可以使用,並且Java類庫中的所有值類都實現了該介面

3)如果你在設計一個類時其需要排序功能,那麼你應該實現Comparable介面

4)compareTo方法的返回值通過值的正值(大於),負值(小於)和零(等於)來代表比較結果

5)compareTo方法也應該保持自反性,對稱性,傳遞性

6)強烈建議(x.compareTo(y)==0)==(x.equals(y)),也即是在等於上建議同equals保持一致

7)在跨越不同類的時候,compareTo可以不做比較,直接排除ClassCastException

8)如果破壞compareTo的約定,則就破壞了依賴compareTo的類似TreeSet,TreeMap的功能

9)有序集合依賴於CompareTo方法,普通集合(Collection,Set或Map)依賴於equals方法,如果你的類沒有遵守這兩個方法的通用約定,那麼在包含這個類的物件的集合可能無法遵守相應集合的通用約定,也就可能使得集合類的工作異常,也就是這些方法的重寫決定著類庫中集合的工作方式,需謹慎

10)Comparable介面是引數化的,而且compareTo方法是靜態的型別,即compareTo中的引數型別同Comparable介面的引數化定義的型別一致,因此不需要進行引數的型別轉換

11)如果一個域沒有實現Comparable介面,或者你需要使用一個非標準的排序關係,那麼可以使用一個顯示的Comparator來代替,使用已有的或者自定義的Comparator

12)多個域的比較類似&&邏輯表示式的截斷功能,只要某個域的比較出現了非零值即可返回,後續的域不需要在進行比較