【Effective java 學習】第三章:對於所有物件都通用的方法
第八條:覆蓋equals是請遵守通用約定
滿足下列四個條件之一,就不需要覆蓋equals方法:
類的每個例項本質上都已唯一的。不包括代表值的類,如:Integer,String等,Object提供的equals方法就夠用了
不關心是否提供了“邏輯相等”的測試功能。對於Random類,使用者只關心函式返回的隨機數,不會關心產生的兩個隨機數是不是相等,所以對其進行equal方法覆蓋將沒有意義
超類已經覆蓋了equals,從超類繼承過來的行為對於子類也是合適的。
類是私有的或是包級私有的,並且確定它的equals方法永遠不會被呼叫。同時為了防止該equals被呼叫,可以如此覆蓋:
@Override public boolean equals(Object obj) { throw new AssertionError(); }
注:此處原文是 類是私有的或包級私有的,可以確定它的equals方法永遠不會被呼叫 。但翻譯的不太合適,原文為
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
傳遞性 。對於任何非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, 999, "area code");
rangeCheck(prefix, 999, "prefix");
rangeCheck(lineNumber, 999, "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 < 0 || 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(707, 867, 5360), "jenny");
//執行下列操作時,雖然已經重寫這個值類的equals方法
//但由於前後兩個物件的hashCode值不同,所以不會執行我們預期的操作
m.get(new PhoneNumber(707, 867, 5360));
一個好的雜湊函式通常傾向於“為不想等的物件產生不想等的雜湊碼”。下面是一種簡單的解決辦法:
把某個非零的常數值,比如說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 = 31 * result + c;
返回result
編寫單元測試來檢驗
在雜湊碼的計算過程中。可以把冗餘域(該域的值可以通過其他域值計算出來)排除在外。同時必須排除equals比較中沒有用到的域。
對於PhoneNumber類,可以這樣覆蓋其hashCode方法
@override
public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * 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 -1;
if (areaCode > pn.areaCode)
return 1;
// area code are equals, compare prefixes
if (prefix < pn.prefix)
return -1;
if (prefix > pn.prefix)
return 1;
//area code and prefixes are equals, compare line number
if (lineNumber < pn.lineNumber)
return -1;
if (lineNumber > pn.lineNumber)
return 1;
//all fields are equals
return 0;
}
如果compareTo的約定沒有指定返回值的大小,而只是指定了返回值的符號,可以對上述程式碼進行簡化
public int compareTo(PhoneNumber pn){
int areaCodeDiff = areaCode - pn.areaCode;
if (areaCodeDiff != 0)
return areaCodeDiff;
int prefixDiff = prefix - pn.prefix;
if (prefixDiff != 0)
return prefixDiff;
return lineNumber - pn.lineNumber;
}
但這種簡化方法,除非確定相關的域不會為負值,或者更一般的情況:最小和最大的可能域值之差小於或者等於INTEGER.MAX_VALUE。