第八條:覆寫equals是請遵守通用約定
滿足下列四個條件之一,就不需要覆寫equals方法:
- 類的每個執行個體本質上都已唯一的。不包括代表值的類,如:Integer,String等,Object提供的equals方法就夠用了
- 不關心是否提供了“邏輯相等”的測試功能。對于Random類,使用者隻關心函數傳回的随機數,不會關心産生的兩個随機數是不是相等,是以對其進行equal方法覆寫将沒有意義
- 超類已經覆寫了equals,從超類繼承過來的行為對于子類也是合适的。
- 類是私有的或是包級私有的,并且确定它的equals方法永遠不會被調用。同時為了防止該equals被調用,可以如此覆寫:
注:此處原文是 類是私有的或包級私有的,可以确定它的equals方法永遠不會被調用 。但翻譯的不太合适,原文為@Override public boolean equals(Object obj) { throw new AssertionError(); }
The class is private or package-private, and you are certain that its equals method will never be invoked.
如果類有自己的“邏輯相等”的概念,通常屬于“值類”的情形,且超類還沒有覆寫equals以實作期望的行為,此時就需要對euqals進行覆寫。但對于“每個值最多隻存在一個對象”的類即單例模式實作的類則不需要覆寫。
在覆寫equals方法時,需要遵守其通用約定:
- 自反性 。對于任何非null的引用值x,x.equals(x)必須==true。
- 對稱性 。對于任何非null的引用值x和y,當且僅當y.equals(x)==true時,x.equals(y)必須==true。
class CaseInsensitiveString { private final String s; public CaseInsensitiveString(String s) { if (s == null){ throw new NullPointerException(); } this.s = s; } //違反了對稱性 //企圖與與普通的String對象進行互操作,但是String類中的equals方法并不知道這個類 //一旦違反了對稱性,當其他對象面對你的對象時,行為是無法知道的 @Override public boolean equals(Object obj) { if (obj instanceof CaseInsensitiveString){ return s.equalgnoreCase(((CaseInsensitiveString) obj).s); } if (obj instanceof String){ return s.equalsIgnoreCase((String) obj); } return false; } }
- 傳遞性 。對于任何非null的引用值x, y和z,如果x.equals(y)==true,并且y.equals(z)==true, 那麼對于x.equals(z)也必須==true。
-
一緻性 。對于任何非null的引用值x和y,隻要equals的比較操作在對象中所用的資訊沒有被修改,多次調用x.equals(y)就會一緻地傳回同樣的結果。
可變對象在不同的時候可以與不同的對象相等,而不可變對象則不會這樣。無論類是否不可變,都不要使用equals方法依賴于不可靠的資源。
- 對于任何非null的引用值x,x.equals(null)必須==false。
實作高品質equals方法的訣竅,下列每一項都是基于前一項:
- 使用==操作符檢查“參數是否為這個對象的引用”。性能優化
- 使用instanceof操作符檢查“參數是否為正确的類型”。
- 把參數轉換成正确的類型。
- 對于該類中的每個“關鍵”域,檢查參數中的域是否對該對象中對應的域相比對。
- 對于float,使用Float.compare;對于double,使用Double.compare
- 有些對象引用域包含null可能是合法的,使用下面的方式避免NullPointException
(field == null ? o.field == null : field.equals(o.field))
- 域的比較可能會影響到equals性能。應先比較最有可能不一緻的域,或是開銷最低的域
- 當你編寫完成了equals方法後,問三個問題:是否是對稱的、傳遞的、一緻的。同時還需編寫測試單元來檢驗。另外兩個特性通常會自動滿足。
最後的告誡:
- 覆寫equals時總要覆寫覆寫hashCode
- 不要企圖讓equals方法過于智能
- 不要将equals聲明中的Object對象替換為其他的類型
第九條:覆寫equals時總要覆寫hashCode
在覆寫equals方法時,如果不覆寫hashCode方法,會導緻該類無法與HashMap、HashSet和HashTable一起正常運作。相等的對象必須具有相等的散列碼。
class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
rangeCheck(areaCode, , "area code");
rangeCheck(prefix, , "prefix");
rangeCheck(lineNumber, , "line number");
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
private static void rangeCheck(int arg, int max, String name) {
if (arg < || arg > max){
throw new IllegalArgumentException(name + ":" + arg);
}
}
@Override
public boolean equals(Object obj) {
if (obj == this){ //使用 == 操作符檢查 “參數是否為這個對象的引用”
return true;
}
if (!(obj instanceof PhoneNumber)){//使用instanceof檢查“參數是否為正确的類型”
return false;
}
PhoneNumber pn = (PhoneNumber) obj;//轉換為正确的類型
//為了獲得最佳性能,先比較最有可能不一樣的域
return pn.lineNumber == lineNumber
&& pn.prefix == prefix
&& pn.areaCode == areaCode;
}
}
在執行下列操作時
Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(, , ), "jenny");
//執行下列操作時,雖然已經重寫這個值類的equals方法
//但由于前後兩個對象的hashCode值不同,是以不會執行我們預期的操作
m.get(new PhoneNumber(, , ));
一個好的散列函數通常傾向于“為不想等的對象産生不想等的散列碼”。下面是一種簡單的解決辦法:
- 把某個非零的常數值,比如說17,儲存在一個名為result的int類型的變量中
- 對于對象中每個關鍵域 f (知equals方法中涉及的每個域),完成以下步驟:
- 為該域計算int類型的散列碼c:
- 如果該域是boolean類型,則計算( f ? 1 : 0)
- 如果該域是byte、char、short或者int類型,計算(int)f
- 如果該域是long類型,則計算(int)(f ^ (f >>> 32))
- 如果該域是float類型,則計算Float.floatToIntBits(f)
- 如果該域是double類型,則計算Double.doubleToLongBits(f),然後按照2計算
- 如果該域是一個對象引用,并且該類的equals方法通過遞歸地調用equals的方式來比較這個域,則同樣為這個域遞歸地調用hashCode。如果需要更複雜的比較,則為這個域計算一個“範式”,然後針對這個範式調用hashCode。如果這個域的值為null,則傳回0(或者其他某個常熟,但通常是0)
- 如果該域是一個數組,則要把每一個元素當作單獨的域來處理。
- 按照下面額公式,把上面步驟中計算的散列碼c合并到result中:
result = * result + c;
- 傳回result
- 編寫單元測試來檢驗
- 為該域計算int類型的散列碼c:
在散列碼的計算過程中。可以把備援域(該域的值可以通過其他域值計算出來)排除在外。同時必須排除equals比較中沒有用到的域。
對于PhoneNumber類,可以這樣覆寫其hashCode方法
@override
public int hashCode() {
int result = ;
result = * result + areaCode;
result = * result + prefix;
result = * result + lineNumber;
return result;
}
如果一個類是不可變的,并且計算散列碼的開銷也比較大,可以考慮把散列碼緩存在對象内部。如果這種類型的大多數對象會被用作散列鍵,就應該在建立執行個體的時候計算散列碼,否則,可以選擇直到hashCode被第一次調用的時候才初始化。
不要試圖從散列碼計算中排除掉一個對象的關鍵部分來提高性能。
第十條:始終要覆寫toString
建議所有的子類都覆寫這個方法。
在實際應用中,toString方法應該傳回對象中包含的所有值得關注的資訊。同時決定是否在文檔中指定傳回值的格式,對于值類,建議這麼做,同時再提供一個相比對的靜态工廠或者構造器,以便于程式員可以很容易的在對象和它的字元串表示法之間轉換,例如:BigInteger、BigDecimal和絕大多數的基本類型包裝類。
但不足之處在于,如果該類被廣泛使用,一旦指定格式,即必須始終堅持這種格式,如果在将來的發行版本中改變,就會破壞代碼和資料。如果不指定格式,就可以保留靈活性,便于在将來的發行版本中增加資訊,或者改進格式。
無論是否指定格式,都應在文檔中明确地表明你的意圖。同時都為toString傳回值中包含的所有資訊,提供一種程式設計式的通路途徑。
第十一條:謹慎地覆寫clone
Cloneable接口中沒有clone方法,Object的clone方法是protected的。Cloneable決定了Object中受保護的clone方法的實作行為:如果一個類實作了Cloneable,Object的clone方法就傳回該對象的逐域拷貝,否則就會抛出CloneNotSupportException。
在克隆對象時,如果每個域包含一個基本類型的值,或者包含一個指向不可變對象的引用,那麼不需要再做進一步處理。如果對象中包含的域引用了可變的對象,此時就需要使用深拷貝。
實際上,clone方法就是另一個構造器,你必須確定它不會傷害到原始的對象,并確定正确地建立被克隆對象中的限制條件。clone架構與引用可變對象的final域的正常用法是不相相容的,因為一個域被final修飾後,就無法再調用clone方法對其進行克隆(即指派)。
克隆複雜對象最後一種辦法是先調用super.clone,然後把結果中的所有域都設定成他們的空白狀态,然後調用高層的方法來重新産生對象的狀态。
覆寫版本的clone方法如果是公有的,就應該将Object中的clone抛出的CloneNotSupportException進行try-catch處理,因為這樣會使得覆寫版本中的clone使用起來更加輕松。如果專門為了繼承而設計的類,就應該模拟Object.clone的行為,這樣使得子類具有實作或不實作Cloneable接口的自由。
如果用線程安全的類實作Cloneable接口,要使得clone方法也有很好的同步。
另一個實作對象拷貝的好辦法是提供一個拷貝構造器,或者拷貝工廠,比起Cloneable/clone有以下優勢:
- 不依賴域某一種很有風險、語言之外的對象建立機制
- 不要求遵守尚未指定好文檔的規範
- 不會與final域的正常使用發生沖突
- 不會抛出不必要的手賤異常
- 不需要進行類型轉換
- 可以帶一個參數,參數類型是通過該類實作的接口。假設你有一個HashSet,并且希望把他拷貝成一個TreeSet,使用轉換構造器:new TreeSet(s)
第十二條:考慮實作Comparable接口
類實作該接口,就表明它的執行個體具有内在的排序關系,可以跟許多泛型算法以及依賴于該接口的集合實作進行協作,java平台類庫中的所有值類都實作了該借口。
将這個對象與指定對象進行比較。當該對象小于、等于或大于指定對象的時候,分别傳回一個負整數、零或正整數。若由于指定對象類型無法比較,則抛出ClassCastException。
說明(sgn為符号函數):
- 必須確定所有的x和y都滿足sgn(x.compareTo(y)) == -sgn(y.compareTo(x))(也暗示着,當且僅當y.compareTo(x)抛出異常時,x.compareTo(y)才必須抛出異常)
- 必須確定此關系可傳遞
- 必須確定x.compareTo(y) == 0 暗示着所有的z都滿足sgn(x.compareTo(z)) == sgn(y.compareTo(z))
-
強烈建議(x.compareTo(y) == 0) == (x.equals(y)),但并非絕對必要。一般來說,任何實作了Comparable接口的類,若違反了這個條件,都應明确予以說明。推薦使用的說法:“注意:該類具有内在排序功能,但與equals不一緻”
違反compareTo約定的類也會破壞其他依賴于比較關系的類,例如TreeSet和TreeMap,以及Collections和Array。
告誡:無法在用新的值組建擴充可執行個體化的類時的同時保持compareTo約定,除非願意放棄面向對象的抽象優勢。 如果想為一個實作了Comparable接口的類增加值組建,要編寫一個不相關的類,其中包含第一個類的一個執行個體。
如果遵守上述“說明”中的最後一條,那麼由compareTo所施加的順序關系就被認為“與equals一緻”,如果違反這條規則,就是“與equals不一緻”,如果不一緻,仍然能正常工作,但如果一個有序集合包含了該類元素,該集合可能就無法遵守相應集合接口(Collection, Set, Map)的通用約定。因為,這些接口的通用約定是按照equals來定義的,但是有序集合使用了由compareTo來定義。例如,new BigDecimal(“1.0”)和new BigDecimal(“1.00”)在HashSet中會兩個都存在,但是在TreeSet中隻存在一個。
Comparable接口是參數化的,而且comparable方法是靜态的類型,不必進行類型檢查,也不必對它的參數進行類型轉換。如果參數不合适,甚至無法編譯。
如果一個類用多個關鍵域,那麼必須從最關鍵的域開始,逐漸進行到所有的重要域。如果某個域的比較産生了非零的結果,則整個比較結束。
public int compareTo(PhoneNumber pn){
if (areaCode < pn.areaCode)
return -;
if (areaCode > pn.areaCode)
return ;
// area code are equals, compare prefixes
if (prefix < pn.prefix)
return -;
if (prefix > pn.prefix)
return ;
//area code and prefixes are equals, compare line number
if (lineNumber < pn.lineNumber)
return -;
if (lineNumber > pn.lineNumber)
return ;
//all fields are equals
return ;
}
如果compareTo的約定沒有指定傳回值的大小,而隻是指定了傳回值的符号,可以對上述代碼進行簡化
public int compareTo(PhoneNumber pn){
int areaCodeDiff = areaCode - pn.areaCode;
if (areaCodeDiff != )
return areaCodeDiff;
int prefixDiff = prefix - pn.prefix;
if (prefixDiff != )
return prefixDiff;
return lineNumber - pn.lineNumber;
}
但這種簡化方法,除非确定相關的域不會為負值,或者更一般的情況:最小和最大的可能域值之差小于或者等于INTEGER.MAX_VALUE。