《Effective Java》學習筆記(二)——對於所有物件都通用的方法
不覆蓋equals方法,類的每個例項都只與它自身相等。如果滿足了以下任何一個條件,就正是所期望的結果:
- 類的每個例項本質上都是唯一的。
- 不關心類是否提供了“邏輯相等”的測試功能。
- 超類已經覆蓋了equals,從超類繼承過來的行為對於子類也是合適的。
- 類是私有的或是包級私有的,可以確定它的equals方法永遠不會被呼叫。
需要覆蓋equals的情況:如果類具有自己特有的“邏輯相等”概念(不同於物件等同的概念),而且超類還沒有覆蓋equals以實現期望的行為。
覆蓋equals方法的時候需要遵守的通用約定(等價關係):
- 自反性:對於任何非null的引用值x,x.equals(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)就會一致地返回true,或者一致地返回false;
- 非空性:對於任何非null的引用值,x.equals(null)必須返回false。
里氏替換原則:一個型別的任何重要屬性也將適用於它的子型別,因此為該型別編寫的任何方法,在它的子型別上也應該同樣執行得很好。
結合這些要求,實現高質量equals方法的訣竅:
- 使用==操作符檢查“引數是否為這個物件的引用”。如果是則返回true。這是一種效能優化,如果比較操作有可能更昂貴,就值得這麼做。
- 使用instanceof操作符檢查“引數是否為正確的型別”。如果不是,則返回false。“正確的型別”是指equals方法所在的那個類,有些情況下,是指該類所實現的某個介面。
- 把引數轉換成正確的型別。因為轉換之前進行過instanceof測試,所以確保會成功。
-
對於該類中的每個“關鍵域”,檢查引數中的域是否與該物件中對應的域相匹配。如果這些測試全部成功,則返回true,否則返回false;
- 對於既不是float也不是double型別的基本型別域,使用==操作符進行比較;
- 對於物件引用域,可以遞迴地呼叫equals方法;
- 對於float域,可以使用Float.compare方法;
- 對於double域,則使用Double.compare方法;對float和double域進行特殊的處理是必要的,因為存在著Float.NaN、-0.0f以及類似的double常量;
- 對於陣列域,則要把以上這些知道原則應用到每個元素上。
- 編寫完成了equals方法之後,要問三個問題:它是否是對稱的、傳遞的、一致的?還要編寫單元測試來檢驗這些特性。
還有一些告誡:
- 覆蓋equals時總要覆蓋hashCode;
- 不要企圖讓equals方法過於智慧;
- 不要講equals宣告中的Object物件替換為其他的型別。
覆蓋equals時總要覆蓋hashCode
在每個覆蓋了equals方法的類中,也必須覆蓋hashCode方法。
Object規範:
- 在應用程式的執行期間,只要物件的equals方法的比較操作所用到的資訊沒有被修改,那麼對這同一個物件呼叫多次,hashCode方法都必須始終如一地返回同一個整數。在同一個應用程式的多次執行過程中,每次執行所返回的整數可以不一致。
- 如果兩個物件根據equals(Object)方法比較是相等的,那麼呼叫這兩個物件中任意一個物件的hashCode方法都必須產生同樣的整數結果。
- 如果兩個物件根據equals(Obejct)方法比較是不相等的,那麼呼叫這兩個物件中任意一個物件的hashCode方法,則不一定要產生不同的整數結果。但是給不相等的物件產生截然不同的整數結果,有可能提高散列表的效能。
如果hashCode方法為不相等的物件產生了很多相等的雜湊碼,那麼雜湊碼相等的這些物件都被對映到同一個雜湊桶中,會是散列表退化成連結串列,極大的影響了散列表的效能。
一個好的雜湊函式通常傾向於“為不相等的物件產生不相等的雜湊碼”。
始終要覆蓋toString
在實際應用中,toString方法應該返回物件中包含的所有值得關注的資訊。無論是否制定輸出的格式,都應該在文件中明確地表明你的意圖。無論是否指定格式,都為toString返回值中包含的所有信息,提供一種程式設計式的訪問途徑。
謹慎地覆蓋clone
Cloneable介面的目的是作為物件的一個mixin介面,表明這樣的物件允許克隆。但是它缺少一個clone方法,Object的clone方法是受保護的。Cloneable介面決定了Object中受保護的clone方法實現的行為:如果一個類實現了Cloneable,Object的clone方法就返回該物件的逐域拷貝,否則就會丟擲CloneNotSupportedException異常。
如果實現Cloneable介面是要對某個類起到作用,類和它的所有超類都必須遵守一個相當複雜、不可實施的,並且基本上沒有文件說明的協議,由此得到一種語言之外的機制:無需呼叫構造器就可以建立物件。
拷貝物件往往會導致建立它的類的一個新例項,但它同時也會要求拷貝內部的資料結構,這個過程沒有呼叫構造器。
如果你覆蓋了非final類中的clone方法,則應該返回一個通過呼叫super.clone而得到的物件。如果類的所有超類都遵守這條規則,那麼呼叫super.clone最終會呼叫Object的clone方法,從而創建出正確類的例項。
實際上,clone方法就是另一個構造器;你必須確保它不會傷害到原始的物件,並確保正確地建立被克隆物件中的約束條件。
如果專門為了繼承而去設計一個clone方法,那就應該模擬Object.clone的行為:它應該被宣告為protected,丟擲CloneNotSupportedException,並且該類不應該實現Cloneable介面。這樣可以使子類具有實現或者不實現Cloneable介面的自由。還有,如果決定用執行緒安全的類實現Cloneable介面,那麼要記得它的clone方法必須實現很好的同步。
Cloneable具有上述這麼多的問題,可以肯定的說,其他的介面都不應該擴充套件這個介面,為了繼承而設計的類也不應該實現這個介面,對於一個為了繼承而設計的類,如果你未能提供行為良好的受保護的clone方法,它的子類就不可能實現Cloneable介面。
考慮實現Comparable介面
compareTo方法是Comparable介面中唯一的方法,compareTo方法不但允許進行簡單的等同性比較,而且允許執行順序比較。類實現了Comparable介面,就表明它的例項具有內在的排序關係。
一旦實現了Comparable介面,它就可以跟許多泛型演算法以及依賴於該介面的集合實現進行協作。Java平臺類庫中的所有值類都實現了Comparable介面。如果你正在編寫一個值類,它具有非常明顯的內在排序關係,比如按字母排序、按數值順序或者按年代排序,那就應該堅決考慮實現這個介面:
public interface Comparable<T> { int compareTo(T t); }
就好像違反了hashCode約定的類會破壞其他依賴於雜湊做法的類一樣,違反compareTo約定的類也會破壞其他依賴於比較關係的類。依賴於比較關係的類包括有序集合類TreeSet和TreeMap,以及工具類Collections和Arrays,他們內部包含有搜尋和排序演算法。
CompareTo方法中域的比較是順序的比較,而不是等同性的比較。比較物件引用域可以使通過遞迴地呼叫compareTo方法來實現。如果一個域沒有實現Comparable介面,或者你需要使用一個非標準的排序關係,就可以使用一個顯式的Comparator來代替,或者編寫自己的Comparator,或者使用已有的Comparator。