Object 通用方法
Object是一個具體類,但是設(shè)計它主要是為了擴(kuò)展,其所有的非final方法(equals、hashCode、toString、clone、finalize)都有明確的通用約定,我們可以在遵守通用約定的前提下override這些方法。
注:不遵守通用約定(general contract)復(fù)寫Object類的非final方法,可以會導(dǎo)致依賴于這些約定的類(hashMap之類的)無法正常工作。
第8條:覆蓋equals時請遵守通用約定
Object的equals原意:類的實例只與它本身相同,即比較地址相同,不比較值。
在以下任意情況下不應(yīng)覆蓋equals方法:
- 類的每個實例本質(zhì)上都是唯一的,即比較地址不比較值(如Thread)
- 不關(guān)心類是否提供了“邏輯相等(值相等)”的測試功能
- 超類已覆蓋了equals方法,且適用于子類(如Set實現(xiàn)類從AbstractSet繼承equals方法,List實現(xiàn)類從AbstractList繼承equals方法)
- 類是私有的或是包級私有的,可以確定其equals不會被調(diào)用。
Tip:禁止調(diào)用可以在對應(yīng)的方法體內(nèi)拋出錯誤
throw new AssertionError()
如果類具有其特有的“邏輯相等”概念。并且超類沒有實現(xiàn)期待的行為,此時我們需要覆蓋equals方法。這通常屬于“值類(value class)”的情況。值類僅僅是一個表示值的類,如Integer或者Date。當(dāng)我們調(diào)用值類的equals時,往往是為了判斷其值是否相等,而不關(guān)心是否為同一個對象(可以使用==判斷是否為同一實例)。
Tip: Set、List、Map等集合類多使用equals判斷key是否相等,所以我們需要在使用這些類時確保我們的equals方法是符合我們的預(yù)期的。
equals方法的通用約定(JavaSE6):
x、y、z皆為非null引用值
- 自反性(reflextive):
assert x.equals(x); - 對稱性(symmetric):
if(x.equals(y)) assert y.equals(x); - 傳遞性(transitive):
if(x.equals(y)&&y.equals(z)) assert x.equals(z); - 一致性(consistent):如果x、y未被修改,則調(diào)用x.equals(y)應(yīng)一直返回true或者一直返回false
- 非空性(Non-nullify):x.equals(null)必須返回false,
assert !x.equals(null)
實現(xiàn)高質(zhì)量equals的訣竅:
- 使用==操作符檢查“參數(shù)是否為這個對象的引用”
- 使用instanceof操作符檢查“參數(shù)是否為正確的類型”
- 把參數(shù)轉(zhuǎn)換為正確的類型
- 對于該類的“關(guān)鍵域”,逐一比較(見以下Tip)
- 回顧并進(jìn)行單元測試:對稱性、傳遞性、一致性
Tip: 逐一比較的訣竅:
1、對于不是float、double的基本類型域,可以直接使用==進(jìn)行比較;
2、對于對象引用域,可以遞歸地調(diào)用equals方法進(jìn)行比較;
3、對于float域,可以使用Float.compare方法;對于double域,可以使用Double.compare方法;
4、對于數(shù)組域,可以使用Array.equals方法
5、使用field == null ? o.field == null : field.equals(o.field)或(field == o.field) || (field != null) && (field.equals(o.field))習(xí)慣寫法
6、優(yōu)化比較順序:優(yōu)先比較最有可能不一致的域、比較開銷最低的域。不比較不屬于對象邏輯狀態(tài)(值)的域,或者冗余域(除非其能代表大量關(guān)鍵域,比較其能節(jié)省大量時間)
最后的告誡:
- 覆蓋equals時總要覆蓋hashCode(見第9條);
- 不要企圖讓equals過于智能;
- 不要將equals聲明中的Object對象替換為其他的類型(這樣是overload而不是override),可以添加
@override注解幫助規(guī)避錯誤。
第9條:覆蓋equals時總要覆蓋hashCode
在Object類中,hashCode方法是一個native方法,返回值與對象的存儲地址相關(guān),計算方法由jvm決定。
hashCode通用公約(JavaSE6):
- 在應(yīng)用程序的執(zhí)行期間,只要對象的equals方法的比較操作所用到的信息沒有被修改,那么對這同一個對象調(diào)用多次,hashCode方法都必須始終如一地返回同一個整數(shù)。在同一個程序多次執(zhí)行過程中,每次執(zhí)行所返回的整數(shù)可以不一致;
- 如果兩個對象根據(jù)equals方法比較是相等的,那么調(diào)用者兩個對象的hashCode方法必須返回同一個整數(shù);
- 如果兩個對象根據(jù)equals方法比較是不相等的,那么調(diào)用者兩個對象的hashCode方法不一定返回不同整數(shù)。給不相等的對象產(chǎn)生截然不同的整數(shù)結(jié)果,有可能提高散列表(hash table)的性能;
注: HashTable、HashMap、HashSet等散列集合類的散列算法實現(xiàn)往往依賴于hashCode。假設(shè)我們覆蓋了equals方法,但沒有覆蓋hashCode,那么在我們理解中equals的對象,對于散列集合類而言,可能是不相等。
高質(zhì)量的hashCode方法的原則:
- 對于equals的對象,返回同一個整數(shù);
- 對于非equals的對象,盡可能返回不一樣的整數(shù);
- 為不相等的對象均勻產(chǎn)生不相等的散列碼。
編寫好質(zhì)量的hashCode的訣竅:
- 把某個非零的常數(shù)值(如17)保存在一個名為result的int類型的變量中;
- 對于對象中的每個關(guān)鍵域f,完成以下步驟:
1、為該域計算int類型的散列碼c(見以下Tip);
2、計算result = 31 * result +c,合并c - 返回result
- 編寫單元測試,保證相等的實例能得到同一個散列值
TIp: 計算int類型的散列碼c
1、如果該域是boolean類型,則計算f ? 1 : 0;
2、如果該域是byte、char、short、int類型,則計算(int) f;
3、如果該域是long類型,則計算(int)( f ^ (f>>>32));
4、如果該域是float類型,則計算Float.floatToTintBits(f);
5、如果該域是long類型,則計算'Double.doubleToLongBits(f)',然后按照步驟3為long計算散列值;
6、如果該域是一個對象引用,為null則返回0,否則遞歸調(diào)用hashCode;
7、如果該域是一個數(shù)組,調(diào)用Arrays.hashCode;
在計算散列碼時,只能用到覆蓋的equals函數(shù)比較的關(guān)鍵域,否則相等(equals)的對象可以會得到不同的散列值。
初始化常數(shù)值17是任選的,不為0就可以了,目的在于增加散列值為0的關(guān)鍵域的影響。
31是一個比較特殊的數(shù)字,首先它是一個奇素數(shù),如果乘數(shù)為偶數(shù),則相當(dāng)于移位,會增加沖突。再者,其乘法可以被jvm自動優(yōu)化:31 * i == (i << 5 ) - i。
Tip:如果一個對象是不可變的,或者其散列值計算開銷比較大,可以考慮將其散列值在實例初始化時計算后緩存在對象內(nèi)部。
第10條:始終要覆蓋toString
在Object類中,toString會返回類名,一個@符號,和散列碼的無符號十六進(jìn)制表示法,這通常不是我們所希望看到的。
toString的通用約定指出,被返回的字符串應(yīng)該是一個簡潔的,信息豐富的,易于閱讀的表達(dá)形式,并建議所有的子類都o(jì)verride這個方法。
提供好的toString實現(xiàn)可以使類用起來更加舒適,特別是在打印對象信息的時候,我們可以之間將對象作為參數(shù)直接傳入print函數(shù),打印出對象信息。
在實際應(yīng)用中,toString方法應(yīng)該返回對象中包含的所有值得關(guān)注的信息
在實現(xiàn)toString時,還應(yīng)考慮是否在文檔中指定返回值的格式。若是指定格式,最好再提供一個靜態(tài)工廠方法或者構(gòu)造器,以便從這種表示法中轉(zhuǎn)換對象;若是不指定返回值的格式,則可以保持toString方法的靈活性,以便在后期改進(jìn)格式。
第11條:謹(jǐn)慎覆蓋clone方法
Cloneable接口的目的在于表明這個對象是允許被clone的,然而它并沒有成功地達(dá)到這個目的。其主要原因在于它并沒有包含任何方法,而Object的clone方法是受保護(hù)的,需要反射調(diào)用,而且反射調(diào)用也不一定會成功。
Cloneable接口的作用在于表明:實現(xiàn)了Cloneable接口的類,應(yīng)該覆蓋了Object的clone方法。
clone方法的通用公約:
創(chuàng)建和返回該對象的一個拷貝。這個拷貝的精確含義由該對象的類決定。一般有以下含義:
- 對于任何對象x,表達(dá)式
x.clone() != x將會是true - 對于任何對象x,表達(dá)式
x.clone().getClass() == x.getClass將會是true - 對于任何對象x,表達(dá)式
x.clone().equals(x)將會是true
拷貝對象往往會導(dǎo)致創(chuàng)建它的類的一個新實例,但它同時要求拷貝類內(nèi)部的數(shù)據(jù)結(jié)構(gòu)。這個過程沒有調(diào)用構(gòu)造器。
覆蓋clone方法,必須保證其拷貝是深度拷貝,這往往需要其超類實現(xiàn)了良好的clone方法。其次,對于拷貝對象內(nèi)部的引用對象,我們需要實例化一個新的對象,并修改其值,而不能簡單復(fù)制引用。覆蓋clone方法是一件十分吃力不討好的事。
所有實現(xiàn)了Cloneable接口的類都應(yīng)該用一個公有的方法覆蓋clone。此公有方法首先調(diào)用super.clone(),然后修正任何需要修正的域。
除非你擴(kuò)展了一個實現(xiàn)Cloneable接口的類,否則最好提供某些其他的途徑代替clone方法,或者干脆不提供這樣的功能。
我們可以提供一個拷貝構(gòu)造器或者拷貝工廠來替代clone方法:
// 拷貝構(gòu)造器:以拷貝對象為參數(shù)
public Yum(Yum yum)
// 拷貝工廠
public static Yum newInstance(Yum yum)
更進(jìn)一步,我們可以提供一個轉(zhuǎn)換構(gòu)造器,其參數(shù)是一個超類/接口的對象。按照慣例,所有通用集合實現(xiàn)都提供了一個轉(zhuǎn)換構(gòu)造器,如一個HashSet s,可以通過TreeSet(s)轉(zhuǎn)換類型。
注意:對于一個專門為了繼承而設(shè)計的類,如果你未能提供香味良好的受保護(hù)的clone方法,它的子類就不可能實現(xiàn)Cloneable方法。
第12條:考慮實現(xiàn)Comparable接口
compareTo方法是Comparable接口唯一的方法,它與Object類的通用方法有很大的相似性。compareTo方法不但允許進(jìn)行簡單的等同性比較,而且允許執(zhí)行順序比較。類實現(xiàn)了Comparable接口,就表明它的實例具有內(nèi)在的排序關(guān)系。對于實現(xiàn)了Comparable接口的對象數(shù)組進(jìn)行排序:Arrays.sort(a)
一旦類實現(xiàn)了Comparable接口,它就可以跟許多泛型算法以及依賴于該接口的集合實現(xiàn)進(jìn)項協(xié)作。事實上,java的所有公共值類都實現(xiàn)了這個接口。假設(shè)你正在編寫一個值類,它具有非常明顯的內(nèi)在排序關(guān)系,那么你就應(yīng)該堅決考慮實現(xiàn)這個接口。
compareTo的通用公約:
將這個對象與指定的對象進(jìn)行比較,當(dāng)對象
- 小于指定對象時,返回負(fù)整數(shù);
- 等于指定對象時,返回0;
- 大于指定對象時,返回正整數(shù);
- 無法與指定對象進(jìn)行比較,拋出
ClassCastException
與equals類似,compareTo同樣有自反型、傳遞性、對稱性
編寫compareTo方法與編寫equals方法非常相似,但也有一些區(qū)別:
- 不用類型檢查與轉(zhuǎn)換,compareTo方法的參數(shù)是靜態(tài)類型,不是Object
- 當(dāng)參數(shù)為null時,應(yīng)該拋出NullPointException