Effective Java:對於所有的物件都通用的方法
Java中所有的類都預設繼承Object類,而Object的設計主要是為了擴充套件。它的所有的非final方法(equals、hashCode、toString、clone和finalize)都有明確的通用約定(general contract),因為它們被設計成要被覆蓋的(override)。任何一個類,它在覆蓋這些方法的時候都有責任遵守這些通用的約定;如果不能做到這一點,其他依賴於這些約定的類就無法結合該類一起正常運作。
本章將講述何時以及如何覆蓋這些非final的Object方法。本章不再討論finalize方法,因為前一篇文章已經討論過了。而Comparable.compareTo雖然不是Object方法,但是本章也對它進行討論,因為它具有類似的特徵。
第八條:覆蓋equals時請遵守通用約定
在覆蓋equals方法的時候,必須遵守它的通用約定。下面是約定內容,來自Object的規範[Java SE6]:
equals方法實現了等價關係(equivalence relation):
- 自反性。對於任何非null的引用值x,x.euqals(x)必須返回true。
- 對稱性。對於任何非null的引用值x和y,當且僅當y.equals(x)返回true時,x.equals(y)必須返回true。
- 傳遞性。對於任何非null的引用值x、y和z,如果x.equals(y)返回true,並且y.equals(z)返回true,那麼x.equals(z)也必須返回true。
- 一致性。對於任何非null的引用值x和y,只要equals的比較操作在物件中所用的資訊沒有被修改,多次呼叫x.equals(y)的結果應該保持一致。
- 對於任何非null的引用值x,x.equals(null)必須返回false。
沒有哪個類是孤立的。一個類的例項通常會被頻繁地傳遞給另一個類的例項。有許多類,包括所有的集合類在內,都依賴於傳遞給他們的物件是否遵守了equals約定。如果違反了它們,程式就會表現的不正常,甚至崩潰,而且很難找到失敗的根源。
結合上述要求,得出了以下實現高質量equals方法的訣竅:
- 使用==操作符檢查“引數是否為這個物件的引用”。
- 使用instanceof操作符檢查“引數是否為正確的型別”。
- 把引數轉換成正確的型別。
- 對於該類中的每個“關鍵(significant)域,檢查引數中的域是否與該物件中對應的域相匹配。
- 當你編寫完成了equals方法之後,就應該問自己:它是否是對稱的、傳遞的、一致的?
下面是最後的一些告誡:
- 覆蓋equals時總要覆蓋hashCode(見第九條)。
- 不要企圖讓equals方法過於智慧。
- 不要講equals宣告中的Object物件替換為其他的型別。
第九條:覆蓋equals時總要覆蓋hashCode
每個覆蓋了equals方法的類中,也必須覆蓋hashCode方法,否則就會違反Object.hashCode的通用約定,從而導致該類無法結合所有基於雜湊的集合一起正常運作,如HashMap、HashSet和HashTable等。
下面是約定的內容,摘自Object規範[Java SE6]:
- 在應用程式的執行期間,只要物件的equals方法的比較操作所用到的資訊沒有被修改,那麼對同一個物件呼叫多次,hashCode方法都必須始終如一地返回同一個整數。在同一個應用程式的多次執行中,每次執行所返回的整數可以不一致。
- 如果兩個物件根據equals(Object)方法比較是相等的,那麼呼叫這兩個物件中任意一個物件的hashCode方法都必須產生同樣的整數結果。
- 如果兩個物件根據equals(Object)方法比較是不相等的,那麼呼叫這兩個物件中任意一個物件的hashCode方法,則不一定產生不同的整數結果。但是程式設計師應該知道,給不相等的物件產生截然不同的整數結果,有可能提高散列表的效能(詳情請參考HashMap雜湊原理)。
因為沒有覆蓋hashCode而違反的關鍵約定是第二條:相等的物件必須具有相等的雜湊值(hash code)。根據累的 equals方法,兩個截然不同的例項在邏輯上有可能是相等的,但是,根據Object類的hashCode方法,它們僅僅是兩個沒有任何共同之處的物件。因此,物件的hashCode方法返回兩個看起來是隨機的整數,而不是根據第二個約定所要求的那樣,返回兩個相等的整數。
下面給出一種簡單的重寫hashCode的方法,這個方法符合在符合第二約定的同時也滿足了第三約定:
- 把某個非零的常數值,比如17,儲存在一個名為result的int型別的變數中。
- 對於物件中的每個關鍵域f(指equals方法中涉及到的每個域),完成以下步驟:
a. 為該域計算int型別的雜湊碼c:
i. 如果該域是boolean型別,則計算(f?1:0).
ii.如果該域是byte、char、short或者int型別,則計算(int)f。
iii.如果該域是long型別,則計算(int)(f^(f>>>32))。
iv.如果該域是float型別,則計算Float.floatToIntBits(f)。
v.如果該域是double型別,則計算Double.doubleToLongBits(f),然後按照步驟iii,為得到的long型別值計算雜湊值。
vi.如果該域是一個物件引用,並且該類的equals方法通過遞迴地呼叫equals的方式來比較這個域,則同樣為這個域遞迴地呼叫hashCode。如果需要更復雜的比較,則為這個域計算一個”正規化(canonical representation)“,然後針對這個正規化呼叫hashCode。如果這個域的值為null,則返回0(或者其他某個常數,但通常是0)。
viii. 如果該域是一個數組,則要把每一個元素當做單獨的域來處理。也就是說,遞迴地應用上述規則,對每個重要的元素計算一個雜湊碼,然後根據步驟2.b中的方法把這些雜湊值組合起來。如果陣列域中國的每個元素都很重要,可以利用JDK中的一個Arrays.hashCode方法。
b.按照下面的公式,把步驟2.a中計算得到的雜湊碼c合併到result中:
result = 31 * result + c; - 返回result。
- 寫完了hashCode方法之後,自問一些”相等的例項是否都具有相等的雜湊碼“。要是不相等,需要找到原因,並修正錯誤。
書中說步驟1中用到了一個非零的初始值,因此步驟2.a中計算的雜湊值為0的那些初始域,會影響到雜湊值,如果步驟1中的初始值為0, 則整個雜湊值將不受這些初始域的影響。
第十條:始終要覆蓋toString
如果不覆蓋toString方法,呼叫x.toString的時候打印出來的將是”類名@雜湊碼“,例如[email protected]。toString的通用約定之處,被返回的字串應該是一個”簡潔的,但資訊豐富,並且易於閱讀的表達形式[Java SE6],建議所有的子類都覆蓋這個方法。
遵守toString約定並不像遵守equals和hashCode的約定那麼重要,後兩者不遵守會導致無法預料的錯誤,而前者只是為了讓類使用起來更加舒適。物件被傳遞給println等方法時,toString方法會被自動呼叫。
實現toString的時候,必須要做出一個很重要的約定:是否在文件中指定返回值的格式,對於值類(value class)也建議這麼做。指定格式的好處是,它可以被用作一種標準的、明確的、適合人閱讀的物件表示法。這種表示法可以用於輸入和輸出,以及用在永久的適合於人類閱讀的資料物件中,例如XML文件。如果指定了格式, 最好再提供一個相匹配的靜態工廠或者構造器,以便程式設計師可以很容易地在物件和它的字串表示法之間來回轉換。Java平臺類庫中的許多值類都採用了這種做法,包括BigInteger、BigDecimal和絕大多數的基本型別包裝類。
第十一條:謹慎地覆蓋clone
Cloneable介面表明這個物件允許克隆。但是這個介面沒有clone方法,Object的clone方法是受保護的。如果不借助於反射,就不能僅僅因為一個物件實現了Cloneable,就可以呼叫clone方法。即使反射呼叫也可能會失敗,因為不能保證該物件一定具有可訪問的clone方法。本條目將討論如何實現一個行為良好的clone方法,並討論何時適合這樣做,同時也簡單地討論了其他的可替換的方法。
Clonable沒有包含任何方法,它只是決定了Object中受保護的clone方法實現的行為:如果一個類實現了Cloneable,Object的clone方法就返回該物件的逐域拷貝,否則就會丟擲CloneNotSupportedException異常。這是介面的一種極端非典型的用法,也不值得仿效。
Clone方法的通用約定是非常弱的,下面是來自java.lang.Object規範中的約定內容[Java SE6]:
建立和返回該物件的一個拷貝。這個“拷貝“的精確含義取決於該物件的類。一般的含義是,對於任何物件x,下面表示式將會是true
x.clone() != x;
x.clone().getClass() == x.getClass();
x.clone.equals(x);
第十二條:考慮實現Comparable介面
compareTo方法並不是Object的方法,它是Comparable介面中的唯一方法。compareTo方法不但允許進行簡單的等同性比較,而且允許執行順序比較,除此之外,它與Object的equals方法具有相似的特徵,它還是個泛型。實現了Comparable介面的類的例項有內在的排序關係。
compareTo方法的通用約定與equals方法的類似:將這個物件與制定的物件進行比較。當該物件小於、等於或大於指定物件的時候,分別返回一個負整數、零或者正整數。如果由於指定物件的型別而無法進行比較,則丟擲ClassCastException異常。
在下面的說明中,符號sgn表示數學中的signum函式,它根據表示式的值為負值、零和正值分別返回-1、0、1.
- 實現者必須確保所有的x和y都滿足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))。當且僅當y.compareTo(x)丟擲異常時,x.compareTo(y)也丟擲異常。
- 實現者必須保證關係是可傳遞的:x.compareTo(y) > 0 && y.compareTo(z) > 0,則x.compareTo(z) > 0。
- 實現者必須確保x.compareTo(y) == 0時,所有的z都滿足sgn(x.compareTo(z)) == sgn(y.compareTo(z))。
- 強烈建議(x.compareTo(y) == 0) == (x.equals(y)),但這非絕對必要。一般說來,任何實現了Comparable介面的類,若違反了這個條件,都應該予以說明。推薦使用這樣的說法:“注意:該類具有內在的排序功能,但是與equals不一致。” -