轉(zhuǎn)載請(qǐng)注明出處: http://blog.csdn.net/javazejian/article/details/51348320 今天這篇文章我們打算來深度解讀一下equal方法以及其關(guān)聯(lián)方法hashCode(),我們準(zhǔn)備從以下幾點(diǎn)入手分析: 1.equals()的所屬以及內(nèi)部原理(即Object中equals方法的實(shí)現(xiàn)原理) 說起equals方法,我們都知道是超類Object中的一個(gè)基本方法,用于檢測(cè)一個(gè)對(duì)象是否與另外一個(gè)對(duì)象相等。而在Object類中這個(gè)方法實(shí)際上是判斷兩個(gè)對(duì)象是否具有相同的引用,如果有,它們就一定相等。其源碼如下:
實(shí)際上我們知道所有的對(duì)象都擁有標(biāo)識(shí)(內(nèi)存地址)和狀態(tài)(數(shù)據(jù)),同時(shí)“==”比較兩個(gè)對(duì)象的的內(nèi)存地址,所以說 Object 的 equals() 方法是比較兩個(gè)對(duì)象的內(nèi)存地址是否相等,即若 object1.equals(object2) 為 true,則表示 equals1 和 equals2 實(shí)際上是引用同一個(gè)對(duì)象。 2.equals()與‘==’的區(qū)別 或許這是我們面試時(shí)更容易碰到的問題”equals方法與‘==’運(yùn)算符有什么區(qū)別?“,并且常常我們都會(huì)胸有成竹地回答:“equals比較的是對(duì)象的內(nèi)容,而‘==’比較的是對(duì)象的地址。”。但是從前面我們可以知道equals方法在Object中的實(shí)現(xiàn)也是間接使用了‘==’運(yùn)算符進(jìn)行比較的,所以從嚴(yán)格意義上來說,我們前面的回答并不完全正確。我們先來看一段代碼并運(yùn)行再來討論這個(gè)問題。
運(yùn)行結(jié)果:
分析:對(duì)于‘==’運(yùn)算符比較兩個(gè)Car對(duì)象,返回了false,這點(diǎn)我們很容易明白,畢竟它們比較的是內(nèi)存地址,而c1與c2是兩個(gè)不同的對(duì)象,所以c1與c2的內(nèi)存地址自然也不一樣?,F(xiàn)在的問題是,我們希望生產(chǎn)的兩輛的批次(batch)相同的情況下就認(rèn)為這兩輛車相等,但是運(yùn)行的結(jié)果是盡管c1與c2的批次相同,但equals的結(jié)果卻反回了false。當(dāng)然對(duì)于equals返回了false,我們也是心知肚明的,因?yàn)閑qual來自O(shè)bject超類,訪問修飾符為public,而我們并沒有重寫equal方法,故調(diào)用的必然是Object超類的原始方equals方法,根據(jù)前面分析我們也知道該原始equal方法內(nèi)部實(shí)現(xiàn)使用的是'=='運(yùn)算符,所以返回了false。因此為了達(dá)到我們的期望值,我們必須重寫Car的equal方法,讓其比較的是對(duì)象的批次(即對(duì)象的內(nèi)容),而不是比較內(nèi)存地址,于是修改如下:
使用instanceof來判斷引用obj所指向的對(duì)象的類型,如果obj是Car類對(duì)象,就可以將其強(qiáng)制轉(zhuǎn)為Car對(duì)象,然后比較兩輛Car的批次,相等返回true,否則返回false。當(dāng)然如果obj不是 Car對(duì)象,自然也得返回false。我們?cè)俅芜\(yùn)行:
嗯,達(dá)到我們預(yù)期的結(jié)果了。因?yàn)榍懊娴拿嬖囶}我們應(yīng)該這樣回答更佳 3.equals()的重寫規(guī)則 前面我們已經(jīng)知道如何去重寫equals方法來實(shí)現(xiàn)我們自己的需求了,但是我們?cè)谥貙慹quals方法時(shí),還是需要注意如下幾點(diǎn)規(guī)則的。
當(dāng)然在通常情況下,如果只是進(jìn)行同一個(gè)類兩個(gè)對(duì)象的相等比較,一般都可以滿足以上5點(diǎn)要求,下面我們來看前面寫的一個(gè)例子。
運(yùn)行結(jié)果:
由運(yùn)行結(jié)果我們可以看出equals方法在同一個(gè)類的兩個(gè)對(duì)象間的比較還是相當(dāng)容易理解的。但是如果是子類與父類混合比較,那么情況就不太簡(jiǎn)單了。下面我們來看看另一個(gè)例子,首先,我們先創(chuàng)建一個(gè)新類BigCar,繼承于Car,然后進(jìn)行子類與父類間的比較。
運(yùn)行結(jié)果:
對(duì)于這樣的結(jié)果,自然是我們意料之中的啦。因?yàn)锽igCar類型肯定是屬于Car類型,所以c.equals(bc)肯定為true,對(duì)于bc.equals(c)返回false,是因?yàn)镃ar類型并不一定是BigCar類型(Car類還可以有其他子類)。嗯,確實(shí)是這樣。但如果有這樣一個(gè)需求,只要BigCar和Car的生產(chǎn)批次一樣,我們就認(rèn)為它們兩個(gè)是相當(dāng)?shù)?,在這樣一種需求的情況下,父類(Car)與子類(BigCar)的混合比較就不符合equals方法對(duì)稱性特性了。很明顯一個(gè)返回true,一個(gè)返回了false,根據(jù)對(duì)稱性的特性,此時(shí)兩次比較都應(yīng)該返回true才對(duì)。那么該如何修改才能符合對(duì)稱性呢?其實(shí)造成不符合對(duì)稱性特性的原因很明顯,那就是因?yàn)镃ar類型并不一定是BigCar類型(Car類還可以有其他子類),在這樣的情況下(Car instanceof BigCar)永遠(yuǎn)返回false,因此,我們不應(yīng)該直接返回false,而應(yīng)該繼續(xù)使用父類的equals方法進(jìn)行比較才行(因?yàn)槲覀兊男枨笫桥蜗嗤?,兩個(gè)對(duì)象就相等,父類equals方法比較的就是batch是否相同)。因此BigCar的equals方法應(yīng)該做如下修改: 這樣運(yùn)行的結(jié)果就都為true了。但是到這里問題并沒有結(jié)束,雖然符合了對(duì)稱性,卻還沒符合傳遞性,實(shí)例如下:
運(yùn)行結(jié)果:
bc,bc2,c的批次都是相同的,按我們之前的需求應(yīng)該是相等,而且也應(yīng)該符合equals的傳遞性才對(duì)。但是事實(shí)上運(yùn)行結(jié)果卻不是這樣,違背了傳遞性。出現(xiàn)這種情況根本原因在于:
只要滿足上面兩個(gè)條件,equals方法的傳遞性便失效了。而且目前并沒有直接的方法可以解決這個(gè)問題。因此我們?cè)谥貙慹quals方法時(shí)這一點(diǎn)需要特別注意。雖然沒有直接的解決方法,但是間接的解決方案還說有滴,那就是通過組合的方式來代替繼承,還有一點(diǎn)要注意的是組合的方式并非真正意義上的解決問題(只是讓它們間的比較都返回了false,從而不違背傳遞性,然而并沒有實(shí)現(xiàn)我們上面batch相同對(duì)象就相等的需求),而是讓equals方法滿足各種特性的前提下,讓代碼看起來更加合情合理,代碼如下:
從代碼來看即使batch相同,Combination4BigCar類的對(duì)象與Car類的對(duì)象間的比較也永遠(yuǎn)都是false,但是這樣看起來也就合情合理了,畢竟Combination4BigCar也不是Car的子類,因此equals方法也就沒必要提供任何對(duì)Car的比較支持,同時(shí)也不會(huì)違背了equals方法的傳遞性。 4.equals()的重寫規(guī)則之必要性深入解讀 前面我們一再?gòu)?qiáng)調(diào)了equals方法重寫必須遵守的規(guī)則,接下來我們就是分析一個(gè)反面的例子,看看不遵守這些規(guī)則到底會(huì)造成什么樣的后果。
上面的代碼,我們聲明了 A,B兩個(gè)類,注意必須是static,否則無法被main調(diào)用。B類繼承A,兩個(gè)類都重寫了equals方法,但是根據(jù)我們前面的分析,這樣重寫是沒有遵守對(duì)稱性原則的,我們先來看看運(yùn)行結(jié)果:
19行和24行的輸出沒什么好說的,將a,b分別加入list中,list中自然會(huì)含有a,b。但是為什么20行和23行結(jié)果會(huì)不一樣呢?我們先來看看contains方法內(nèi)部實(shí)現(xiàn) 進(jìn)入indexof方法
可以看出最終調(diào)用的是對(duì)象的equals方法,所以當(dāng)調(diào)用20行代碼list.contains(b)時(shí),實(shí)際上調(diào)用了 b.equals(a[i]),a[i]是集合中的元素集合中的類型而且為A類型(只添加了a對(duì)象),雖然B繼承了A,但此時(shí) 結(jié)果為false,equals方法也就會(huì)返回false;而當(dāng)調(diào)用23行代碼list.contains(a)時(shí),實(shí)際上調(diào)用了a.equal(a[i]),其中a[i]是集合中的元素而且為B類型(只添加了b對(duì)象),由于B類型肯定是A類型(B繼承了A),所以 結(jié)果為true,equals方法也就會(huì)返回true,這就是整個(gè)過程。但很明顯結(jié)果是有問題的,因?yàn)槲覀兊?list的泛型是A,而B又繼承了A,此時(shí)無論加入了a還是b,都屬于同種類型,所以無論是contains(a),還是contains(b)都應(yīng)該返回true才算正常。而最終卻出現(xiàn)上面的結(jié)果,這就是因?yàn)橹貙慹quals方法時(shí)沒遵守對(duì)稱性原則導(dǎo)致的結(jié)果,如果沒遵守傳遞性也同樣會(huì)造成上述的結(jié)果。當(dāng)然這里的解決方法也比較簡(jiǎn)單,我們只要將B類的equals方法修改一下就可以了。
到此,我們也應(yīng)該明白了重寫equals必須遵守幾點(diǎn)原則的重要性了。當(dāng)然這里不止是list,只要是java集合類或者java類庫(kù)中的其他方法,重寫equals不遵守5點(diǎn)原則的話,都可能出現(xiàn)意想不到的結(jié)果。 5.為什么重寫equals()的同時(shí)還得重寫hashCode() 這個(gè)問題之前我也很好奇,不過最后還是在書上得到了比較明朗的解釋,當(dāng)然這個(gè)問題主要是針對(duì)映射相關(guān)的操作(Map接口)。學(xué)過數(shù)據(jù)結(jié)構(gòu)的同學(xué)都知道Map接口的類會(huì)使用到鍵對(duì)象的哈希碼,當(dāng)我們調(diào)用put方法或者get方法對(duì)Map容器進(jìn)行操作時(shí),都是根據(jù)鍵對(duì)象的哈希碼來計(jì)算存儲(chǔ)位置的,因此如果我們對(duì)哈希碼的獲取沒有相關(guān)保證,就可能會(huì)得不到預(yù)期的結(jié)果。在java中,我們可以使用hashCode()來獲取對(duì)象的哈希碼,其值就是對(duì)象的存儲(chǔ)地址,這個(gè)方法在Object類中聲明,因此所有的子類都含有該方法。那我們先來認(rèn)識(shí)一下hashCode()這個(gè)方法吧。hashCode的意思就是散列碼,也就是哈希碼,是由對(duì)象導(dǎo)出的一個(gè)整型值,散列碼是沒有規(guī)律的,如果x與y是兩個(gè)不同的對(duì)象,那么x.hashCode()與y.hashCode()基本是不會(huì)相同的,下面通過String類的hashCode()計(jì)算一組散列碼:
運(yùn)行結(jié)果:
我們可以看出,字符串s與t擁有相同的散列碼,這是因?yàn)樽址纳⒘写a是由內(nèi)容導(dǎo)出的。而字符串緩沖sb與tb卻有著不同的散列碼,這是因?yàn)镾tringBuilder沒有重寫hashCode方法,它的散列碼是由Object類默認(rèn)的hashCode方法計(jì)算出來的對(duì)象存儲(chǔ)地址,所以散列碼自然也就不同了。那么我們?cè)撊绾沃貙懗鲆粋€(gè)較好的hashCode方法呢,其實(shí)并不難,我們只要合理地組織對(duì)象的散列碼,就能夠讓不同的對(duì)象產(chǎn)生比較均勻的散列碼。例如下面的例子: 上面的代碼我們通過合理的利用各個(gè)屬性對(duì)象的散列碼進(jìn)行組合,最終便能產(chǎn)生一個(gè)相對(duì)比較好的或者說更加均勻的散列碼,當(dāng)然上面僅僅是個(gè)參考例子而已,我們也可以通過其他方式去實(shí)現(xiàn),只要能使散列碼更加均勻(所謂的均勻就是每個(gè)對(duì)象產(chǎn)生的散列碼最好都不沖突)就行了。不過這里有點(diǎn)要注意的就是java 7中對(duì)hashCode方法做了兩個(gè)改進(jìn),首先java發(fā)布者希望我們使用更加安全的調(diào)用方式來返回散列碼,也就是使用null安全的方法Objects.hashCode(注意不是Object而是java.util.Objects)方法,這個(gè)方法的優(yōu)點(diǎn)是如果參數(shù)為null,就只返回0,否則返回對(duì)象參數(shù)調(diào)用的hashCode的結(jié)果。Objects.hashCode 源碼如下: 因此我們修改后的代碼如下: java 7還提供了另外一個(gè)方法java.util.Objects.hash(Object... objects),當(dāng)我們需要組合多個(gè)散列值時(shí)可以調(diào)用該方法。進(jìn)一步簡(jiǎn)化上述的代碼:
好了,到此hashCode()該介紹的我們都說了,還有一點(diǎn)要說的如果我們提供的是一個(gè)數(shù)值類型的變量的話,那么我們可以調(diào)用Arrays.hashCode()來計(jì)算它的散列碼,這個(gè)散列碼是由數(shù)組元素的散列碼組成的。接下來我們回歸到我們之前的問題,重寫equals方法時(shí)也必須重寫hashCode方法。在Java API文檔中關(guān)于hashCode方法有以下幾點(diǎn)規(guī)定(原文來自java深入解析一書)。
通過前面的分析,我們知道在Object類中,hashCode方法是通過Object對(duì)象的地址計(jì)算出來的,因?yàn)镺bject對(duì)象只與自身相等,所以同一個(gè)對(duì)象的地址總是相等的,計(jì)算取得的哈希碼也必然相等,對(duì)于不同的對(duì)象,由于地址不同,所獲取的哈希碼自然也不會(huì)相等。因此到這里我們就明白了,如果一個(gè)類重寫了equals方法,但沒有重寫hashCode方法,將會(huì)直接違法了第2條規(guī)定,這樣的話,如果我們通過映射表(Map接口)操作相關(guān)對(duì)象時(shí),就無法達(dá)到我們預(yù)期想要的效果。如果大家不相信, 可以看看下面的例子(來自java深入解析一書) 代碼比較簡(jiǎn)單,我們就不過多解釋了(注意Key類并沒有重寫hashCode方法),直接運(yùn)行看結(jié)果 對(duì)于s1和s2的結(jié)果,我們并不驚訝,因?yàn)橄嗤膬?nèi)容的s1和s2獲取相同內(nèi)的value這個(gè)很正常,因?yàn)镾tring類重寫了equals方法和hashCode方法,使其比較的是內(nèi)容和獲取的是內(nèi)容的哈希碼。但是對(duì)于k1和k2的結(jié)果就不太盡人意了,k1獲取到的值是2,k2獲取到的是null,這是為什么呢?想必大家已經(jīng)發(fā)現(xiàn)了,Key只重寫了equals方法并沒有重寫hashCode方法,這樣的話,equals比較的確實(shí)是內(nèi)容,而hashCode方法呢?沒重寫,那就肯定調(diào)用超類Object的hashCode方法,這樣返回的不就是地址了嗎?k1與k2屬于兩個(gè)不同的對(duì)象,返回的地址肯定不一樣,所以現(xiàn)在我們知道調(diào)用map2.get(k2)為什么返回null了吧?那么該如何修改呢?很簡(jiǎn)單,我們要做也重寫一下hashCode方法即可(如果參與equals方法比較的成員變量是引用類型的,則可以遞歸調(diào)用hashCode方法來實(shí)現(xiàn)): 再次運(yùn)行:
6.重寫equals()中g(shù)etClass與instanceof的區(qū)別 雖然前面我們都在使用instanceof(當(dāng)然前面我們是根據(jù)需求(批次相同即相等)而使用instanceof的),但是在重寫equals() 方法時(shí),一般都是推薦使用 getClass 來進(jìn)行類型判斷(除非所有的子類有統(tǒng)一的語(yǔ)義才使用instanceof),不是使用 instanceof。我們都知道 instanceof 的作用是判斷其左邊對(duì)象是否為其右邊類的實(shí)例,返回 boolean 類型的數(shù)據(jù)。可以用來判斷繼承中的子類的實(shí)例是否為父類的實(shí)現(xiàn)。下來我們來看一個(gè)例子:父類Person 子類 Employee 上面父類 Person 和子類 Employee 都重寫了 equals(),不過 Employee 比父類多了一個(gè)id屬性,而且這里我們并沒有統(tǒng)一語(yǔ)義。測(cè)試代碼如下:
上面代碼我們定義了兩個(gè)員工和一個(gè)普通人,雖然他們同名,但是他們肯定不是同一人,所以按理來說結(jié)果應(yīng)該全部是
false,但是事與愿違,結(jié)果是:true、true、false。對(duì)于那 e1!=e2 我們非常容易理解,因?yàn)樗麄儾粌H需要比較 name,還需要比較 ID。但是 p1 即等于 e1 也等于 e2,這是非常奇怪的,因?yàn)?e1、e2 明明是兩個(gè)不同的類,但為什么會(huì)出現(xiàn)這個(gè)情況?首先 p1.equals(e1),是調(diào)用 p1 的 equals 方法,該方法使用 instanceof 關(guān)鍵字來檢查 e1 是否為 Person 類,這里我們?cè)倏纯?instanceof:判斷其左邊對(duì)象是否為其右邊類的實(shí)例,也可以用來判斷繼承中的子類的實(shí)例是否為父類的實(shí)現(xiàn)。他們兩者存在繼承關(guān)系,肯定會(huì)返回
true 了,而兩者 name 又相同,所以結(jié)果肯定是 true。所以出現(xiàn)上面的情況就是使用了關(guān)鍵字 instanceof,這是非常容易導(dǎo)致我們“鉆牛角尖”。故在覆寫 equals 時(shí)推薦使用 getClass 進(jìn)行類型判斷。而不是使用 instanceof(除非子類擁有統(tǒng)一的語(yǔ)義)。
7.編寫一個(gè)完美equals()的幾點(diǎn)建議 下面給出編寫一個(gè)完美的equals方法的建議(出自Java核心技術(shù) 第一卷:基礎(chǔ)知識(shí)): 1)顯式參數(shù)命名為otherObject,稍后需要將它轉(zhuǎn)換成另一個(gè)叫做other的變量(參數(shù)名命名,強(qiáng)制轉(zhuǎn)換請(qǐng)參考建議5) 2)檢測(cè)this與otherObject是否引用同一個(gè)對(duì)象 :if(this == otherObject) return true;(存儲(chǔ)地址相同,肯定是同個(gè)對(duì)象,直接返回true) 3) 檢測(cè)otherObject是否為null ,如果為null,返回false.if(otherObject == null) return false; 4) 比較this與otherObject是否屬于同一個(gè)類 (視需求而選擇)
5) 將otherObject轉(zhuǎn)換為相應(yīng)的類類型變量:ClassName other = (ClassName) otherObject; 6) 現(xiàn)在開始對(duì)所有需要比較的域進(jìn)行比較 。使用==比較基本類型域,使用equals比較對(duì)象域。如果所有的域都匹配,就返回true,否則就返回flase。
參考資料: Java深入分析 http://wiki./project/java-enhancement/java-thirteen.html
|
|