本章主要講的是如何覆蓋一些非final的Object方法:
equals/hashCode/toString/clone方法
第八條、覆蓋equals時請遵守通用約定
1. 不覆蓋equals方法的條件(滿足其一即可):
類的每個實例本質上都是唯一的。代表的是活動實體而不是值,例如Thread;
不關心類是否提供邏輯相等的測試功能。如java.util.Random;
父類已經覆蓋了equals,從父類繼承過來的行為對于子類也是合適的。例如:大部分的Set實現都從AbstractSet繼承equals實現。
類是私有的或者包級私有的,可以確定它的equals方法永遠不會被調用。
2. 什么時候應該覆蓋equals方法:
如果類具有自己特有的邏輯相等概念,且其父類還沒有覆蓋equals以實現其期望的行為,則需要覆蓋equals方法。
通常屬于“值類”的情形,僅僅是表示值的類,如Integer和Date。這樣做可以使得這個類的實例可以被用作映射表的鍵key,或者集合set的元素。
3. Object中的equals規范(等價關系equivalence relation):
自反性(reflexive):x.equals(x)必須返回true,x非空。
對稱性(symmetric):當且僅當x.equals(y)返回true時,y.equals(x)必須返回true。x,y非空。
傳遞性(transitive):x.equals(y)返回true,且y.equals(z)返回true,那么x.equals(z)也必須返回為true。 x,y,z非空。
一致性(consistent):只要比較操作在對象中所用的信息沒有被修改,則多次調用的結果會一致。
對于任何非nul的引用值x,x.equals(null)必須返回false。
4. 等價關系的一個基本問題:
我們無法在擴展可實例化的類的同時,既增加新的值組件,同時又保留equals約定。除非愿意放棄面向對象的抽象帶來的優勢。(利用復合來解決類似的問題)
5. 實現高質量equals方法的訣竅:
使用==操作符檢查”參數是非為這個對象的引用“,如果是,則返回true;
使用instanceof操作否檢查”參數是否為正確的類型“;
把參數轉換成正確的類型;
對于該類中的每個關鍵域,檢查參數中的域是否與該對象中對應的域相匹配。域的比較順序可能影響到equals方法的性能,應最先比較最有可能不一致的域或者是開銷最小的域。
完成后檢查是否符合:對稱性,傳遞性和一致性。
覆蓋equals時總要覆蓋hashCode方法。
不要企圖使equals方法過于智能。
不要將equals聲明中的Object對象替換為其他的類型。原因是這個方法并沒有覆蓋Object.equals!參數類型不正確。在原有的equals方法的基礎上,重載了一個強類型的equals方法。@Override注解在這里面就起作用了。
第九條、覆蓋equals時總要覆蓋hashCode
1. 在每個覆蓋了equals方法的類中,也必須覆蓋hashCode方法。
否則會違反Object.hashCode的通用約定,導致該類無法結合所有基于散列的集合一起正常工作(HashMap、HashSet和HashTable)。
2. Object約束中hashCode的約定:
在應用程序執行期間,只要對象的equals方法的比較操作所用到的信息沒有被修改,那么對這同一對象調用多次,hashCode方法都必須始終如一地返回同一個整數。在同一個應用程序的多次執行過程中,每次執行所返回的整數可以不一致。
如果兩個對象根據equals(Object)方法比較是相等的,則調用這兩個對象中任意一個對象的hashCode方法都必須產生相同的整數結果。
如果兩個對象根據equals(Object)方法比較是不相等的,hashCode不一定產生不同的整數結果。
3. 一個好的散列函數“為不相等的對象產生不相等的散列碼”。
理想情況下,散列函數應該把集合中不相等的實例均勻地分布到所有可能的散列值,要想完全達到這種理想的情形是非常困難的,下面有一種簡單的解決方法:
- 把某個非零的常數值(如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),然后按照long類型計算
- 該域是一個對象引用,針對范式調用hashCode,再進行判斷。
- 該域是一個數組,Arrays.hashCode方法
- 按照下面的公式,把上步中計算的散列碼合并到result中:(使得散列值依賴于域的順序)
- result = 31 * result + c ;
- 返回result
下面是一個實例:
import java.util.HashMap;
import java.util.Map;
/**
* Created by laneruan on 2017/6/6.
*/
public class PhoneNumberHashCode{
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumberHashCode(int areaCode,int prefix,int lineNumber){
rangeCheck(areaCode,999,"area code");
rangeCheck(prefix,999,"prefix");
rangeCheck(lineNumber,999,"lineNumber");
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 o){
if(o == this)
return true;
if(!(o instanceof PhoneNumberHashCode))
return false;
PhoneNumberHashCode pnhc = (PhoneNumberHashCode) o;
return pnhc.lineNumber == lineNumber &&
pnhc.prefix == prefix &&
pnhc.areaCode == areaCode;
}
@Override
public int hashCode(){
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
//如果沒有hashCode方法,會出現如下問題
// Map<PhoneNumber,String> map = new HashMap<PhoneNumber,String>();
// map.put(new PhoneNumber(701,867,5309),"Jenny");
// map.get(new PhoneNumber(707,867,5309)) 返回為null
//Lazy initialized,cached hashCode
private volatile int hashCode;
public int hashCode2(){
int result = hashCode;
if(result == 0){
result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
}
return result;
}
}
4. 如果一個類是不可變的,且計算散列碼的開銷比較大,可以考慮把散列碼緩存在對象內部,而不是每次請求都重新計算散列碼。
第十條、始終要覆蓋toString
java.lang.Object提供的toString方法的一個實現,但通常不是用戶希望看到的:包含一個類名稱,一個@符號接著是散列碼的無符號十六進制表示法。“簡潔的、但信息豐富且易于閱讀的表達形式,建議所有的子類都覆蓋這一方法。”當對象被傳遞給println,printf,字符串聯操作符+、assert或者被調試器打印出來時,toString會自動調用。
在實際應用中,toString方法應該返回對象中包含的所有值得關注的信息。
無論是否指定格式,都用該在文檔中明確表明你的意圖;都應為toString返回中包含的所有信息,提供一種編程式的訪問途徑。
第十一條、謹慎地覆蓋clone
1. Cloneable接口的目的是作為對象的一個mixin接口,表明這個對象允許克隆。
其主要的缺陷在于缺少一個clone方法,Object的clone方法是受保護的,如果不加借助于反射,就不能因為一個對象實現了Cloneable接口,就可以調用clone方法。
這個接口的作用在于決定了Object中受保護的clone方法實現的行為:如果一個類實現了Cloneable,Object的clone方法就返回該對象的逐域拷貝,否則就會拋出CloneNotSupportException。
這樣的副作用是:無需調用構造器就可以創建對象。
2. java.lang.Object關于Clone方法的約定:
創建和返回該對象的一個拷貝,這個拷貝的精確含義取決于該對象的類。
一般的來說是:對于任何對象x,表達式x.clone() != x 為true 且 x.clone().getClass() == x.getClass() 為true,x.clone().equals(x)為true,但這些都不是絕對的要求。
拷貝對象往往會創建它的類的一個實例,但它同時也會要求拷貝內部的數據結構,過程中沒有調用構造器!
3. 實際上,對于實現了Cloneable接口的類,我們總是期望它也提供一個功能適當的公有的clone方法。
從super.clone()中得到的對象可能會接近最終要返回的對象,也可能相差甚遠,這取決于類的本質。如果每個域包含一個基本類型的值,或者包含一個指向不可變對象的引用,那么可能不需要再做進一步處理。
如果對象中包含的域引用了可變的對象,使用上述簡單的clone實現可能會出現災難性的后果。一般需要在這些可變的對象中遞歸地調用clone。實際上clone方法相當于另一個構造器,你必須確保它不會傷害到原始的對象,并確保正確地創建被克隆對象中的約束條件(invariant)。clone架構與引用可變對象的final域的正常用法是不相兼容的。
克隆復雜對象的最后一種方法是:先調用super.clone(),然后把結果中的所有域都設置成它們的空白狀態,然后調用高層的方法來重新產生對象的狀態。
總結下來:所有實現了Cloneable接口的類都應該用一個公有的方法覆蓋clone,此公有方法首先調用super.clone,然后修正任何需要修正的域。那么真的有必要這么復雜嗎?很少!
4. 我們平時最好提供某些其他的途徑來代替對象拷貝,或者根本不提供這樣的功能。
比如對于不可變類,支持對象拷貝意義不大。
另一種提供對象拷貝的好方法是提供一個拷貝構造器(copy constructor)或拷貝工廠。這種做法優勢很大。所以請謹慎地覆蓋clone。
如:public Yum(Yum yum);public static Yum newInstance(Yum yum)
第十二條、考慮實現Comparable接口
compareTo方法沒有在Object中聲明,它是Comparable接口中唯一的方法。compareTo方法不但允許進行簡單的等同性比較,而且允許執行順序比較,它還是個泛型。
類實現了Comparable接口,就表明它的實例具有內在的排序關系,為實現了Comparable接口的對象數組進行排序可以直接
Arrays.sotr(a)
。對存儲在集合中的Comparable對象進行搜索、計算極限值以及自動維護也同樣簡單。一旦類實現了Comparable接口,它就可以跟許多泛型算法以及依賴于該接口的集合實現進行協作。事實上,Java平臺類庫中所有的值類都實現了Comparable接口。-
什么時候考慮實現Comparable接口:正在編寫一個值類,它具有非常明顯的內在排序關系,比如按照字母排序、數值排序或者年代排序,那就應該堅決考慮實現這個接口:
public interface Comparable<T>{ int compareTo(T t); }
-
compareTo方法的通用約定:將這個對象與指定的對象進行比較。當該對象小于、等于或者大于指定對象的時候,分別返回一個負整數、零或者正整數、如果由于指定對象的類型而無法與該對象進行比較,則拋出ClassCastException異常。符號sgn(表達式)表示數學中的signum函數,根據表達式的值為負數、零和正值,分別返回-1、0或1。
- 實現者必須確保所有的x和y都滿足:
sgn(x.compareTo(y)) == -sign(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))
。如果違反這個條件請予以說明:”注意:該類具有內在的排序功能,但與equals不一致。“ 違反compareTo約定的類會破壞依賴于比較關系的類包括有序集合類TreeSet和TreeMap,以及工具類Collections和Arrays,它們內部包含有搜索和排序算法。
- 實現者必須確保所有的x和y都滿足:
-
如果一個類有多個關鍵域,那么按什么樣的順序來比較這些域是非常關鍵的,必須從最關鍵的域開始,逐步進行到所有的重要域。
下面是關于電話類的一個實例:@Override public int compareTo(PhoneNumberHashCode 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; }