重寫(xiě)hashCode()和equals()方法詳細(xì)介紹
hashCode()和equals()方法可以說(shuō)是Java完全面向?qū)ο蟮囊淮筇厣鼮槲覀兊木幊烫峁┍憷耐瑫r(shí)也帶來(lái)了很多危險(xiǎn).這篇文章我們就討論一下如何正解理解和使用這2個(gè)方法.
如何重寫(xiě)equals()方法
如果你決定要重寫(xiě)equals()方法,那么你一定要明確這么做所帶來(lái)的風(fēng)險(xiǎn),并確保自己能寫(xiě)出一個(gè)健壯的equals()方法.一定要注意的一點(diǎn)是,在重寫(xiě)equals()后,一定要重寫(xiě)hashCode()方法.具體原因稍候再進(jìn)行說(shuō)明.
我們先看看 JavaSE 7 Specification中對(duì)equals()方法的說(shuō)明:
·It is reflexive: for any non-null reference value x, x.equals(x)
should return true
.
·It is symmetric: for any non-null reference values x
and y, x.equals(y)
should return true
if and only if y.equals(x)
returns true
.
·It is transitive: for any non-null reference values x, y,
and z, if x.equals(y)
returns true and y.equals(z)
returns true
, then x.equals(z)
should return true.
·It is consistent: for any non-null reference values x
and y
, multiple invocations of x.equals(y)
consistently return true
or consistently return false
, provided no information used in equals comparisons on the objects is modified.
·For any non-null reference value x, x.equals(null)
should return false
.
這段話用了很多離散數(shù)學(xué)中的術(shù)數(shù).簡(jiǎn)單說(shuō)明一下:
1. 自反性:A.equals(A)要返回true.
2. 對(duì)稱性:如果A.equals(B)返回true, 則B.equals(A)也要返回true.
3. 傳遞性:如果A.equals(B)為true, B.equals(C)為true, 則A.equals(C)也要為true. 說(shuō)白了就是 A = B , B = C , 那么A = C.
4. 一致性:只要A,B對(duì)象的狀態(tài)沒(méi)有改變,A.equals(B)必須始終返回true.
5. A.equals(null) 要返回false.
相信只要不是專業(yè)研究數(shù)學(xué)的人,都對(duì)上面的東西不來(lái)電.在實(shí)際應(yīng)用中我們只需要按照一定的步驟重寫(xiě)equals()方法就可以了.為了說(shuō)明方便,我們先定義一個(gè)程序員類(lèi)(Coder):
class Coder { private String name; private int age; // getters and setters }
我們想要的是,如果2個(gè)程序員對(duì)象的name和age都是相同的,那么我們就認(rèn)為這兩個(gè)程序員是一個(gè)人.這時(shí)候我們就要重寫(xiě)其equals()方法.因?yàn)槟J(rèn)的equals()實(shí)際是判斷兩個(gè)引用是否指向內(nèi)在中的同一個(gè)對(duì)象,相當(dāng)于 == . 重寫(xiě)時(shí)要遵循以下三步:
1. 判斷是否等于自身.
if(other == this) return true;
2. 使用instanceof運(yùn)算符判斷 other 是否為Coder類(lèi)型的對(duì)象.
if(!(other instanceof Coder)) return false;
3. 比較Coder類(lèi)中你自定義的數(shù)據(jù)域,name和age,一個(gè)都不能少.
Coder o = (Coder)other; return o.name.equals(name) && o.age == age;
看到這有人可能會(huì)問(wèn),第3步中有一個(gè)強(qiáng)制轉(zhuǎn)換,如果有人將一個(gè)Integer類(lèi)的對(duì)象傳到了這個(gè)equals中,那么會(huì)不會(huì)扔ClassCastException呢?這個(gè)擔(dān)心其實(shí)是多余的.因?yàn)槲覀冊(cè)诘诙街幸呀?jīng)進(jìn)行了instanceof 的判斷,如果other是非Coder對(duì)象,甚至other是個(gè)null, 那么在這一步中都會(huì)直接返回false, 從而后面的代碼得不到執(zhí)行的機(jī)會(huì).
上面的三步也是<Effective Java>中推薦的步驟,基本可保證萬(wàn)無(wú)一失.
如何重寫(xiě)hashCode()方法
在JavaSE 7 Specification中指出,
"Note that it is generally necessary to override the hashCode method whenever this method(equals) is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes."
如果你重寫(xiě)了equals()方法,那么一定要記得重寫(xiě)hashCode()方法.我們?cè)诖髮W(xué)計(jì)算機(jī)數(shù)據(jù)結(jié)構(gòu)課程中都已經(jīng)學(xué)過(guò)哈希表(hash table)了,hashCode()方法就是為哈希表服務(wù)的.
當(dāng)我們?cè)谑褂眯稳鏗ashMap, HashSet這樣前面以Hash開(kāi)頭的集合類(lèi)時(shí),hashCode()就會(huì)被隱式調(diào)用以來(lái)創(chuàng)建哈希映射關(guān)系.稍后我們?cè)賹?duì)此進(jìn)行說(shuō)明.這里我們先重點(diǎn)關(guān)注一下hashCode()方法的寫(xiě)法.
<Effective Java>中給出了一個(gè)能最大程度上避免哈希沖突的寫(xiě)法,但我個(gè)人認(rèn)為對(duì)于一般的應(yīng)用來(lái)說(shuō)沒(méi)有必要搞的這么麻煩.如果你的應(yīng)用中HashSet中需要存放上萬(wàn)上百萬(wàn)個(gè)對(duì)象時(shí),那你應(yīng)該嚴(yán)格遵循書(shū)中給定的方法.如果是寫(xiě)一個(gè)中小型的應(yīng)用,那么下面的原則就已經(jīng)足夠使用了:
要保證Coder對(duì)象中所有的成員都能在hashCode中得到體現(xiàn).
對(duì)于本例,我們可以這么寫(xiě):
@Override public int hashCode() { int result = 17; result = result * 31 + name.hashCode(); result = result * 31 + age; return result; }
其中int result = 17你也可以改成20, 50等等都可以.看到這里我突然有些好奇,想看一下String類(lèi)中的hashCode()方法是如何實(shí)現(xiàn)的.查文檔知:
"Returns a hash code for this string. The hash code for a String object is computed as
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
using int arithmetic, where s[i] is the ith character of the string, n is the length of the string, and ^ indicates exponentiation. (The hash value of the empty string is zero.)"
對(duì)每個(gè)字符的ASCII碼計(jì)算n - 1次方然后再進(jìn)行加和,可見(jiàn)Sun對(duì)hashCode的實(shí)現(xiàn)是很?chē)?yán)謹(jǐn)?shù)? 這樣能最大程度避免2個(gè)不同的String會(huì)出現(xiàn)相同的hashCode的情況.
重寫(xiě)equals()而不重寫(xiě)hashCode()的風(fēng)險(xiǎn)
在Oracle的Hash Table實(shí)現(xiàn)中引用了bucket的概念.如下圖所示:
從上圖中可以看出,帶bucket的hash table大致相當(dāng)于哈希表與鏈表的結(jié)合體.即在每個(gè)bucket上會(huì)掛一個(gè)鏈表,鏈表的每個(gè)結(jié)點(diǎn)都用來(lái)存放對(duì)象.Java通過(guò)hashCode()方法來(lái)確定某個(gè)對(duì)象應(yīng)該位于哪個(gè)bucket中,然后在相應(yīng)的鏈表中進(jìn)行查找.在理想情況下,如果你的hashCode()方法寫(xiě)的足夠健壯,那么每個(gè)bucket將會(huì)只有一個(gè)結(jié)點(diǎn),這樣就實(shí)現(xiàn)了查找操作的常量級(jí)的時(shí)間復(fù)雜度.即無(wú)論你的對(duì)象放在哪片內(nèi)存中,我都可以通過(guò)hashCode()立刻定位到該區(qū)域,而不需要從頭到尾進(jìn)行遍歷查找.這也是哈希表的最主要的應(yīng)用.
如:
當(dāng)我們調(diào)用HashSet的put(Object o)方法時(shí),首先會(huì)根據(jù)o.hashCode()的返回值定位到相應(yīng)的bucket中,如果該bucket中沒(méi)有結(jié)點(diǎn),則將 o 放到這里,如果已經(jīng)有結(jié)點(diǎn)了, 則把 o 掛到鏈表末端.同理,當(dāng)調(diào)用contains(Object o)時(shí),Java會(huì)通過(guò)hashCode()的返回值定位到相應(yīng)的bucket中,然后再在對(duì)應(yīng)的鏈表中的結(jié)點(diǎn)依次調(diào)用equals()方法來(lái)判斷結(jié)點(diǎn)中的對(duì)象是否是你想要的對(duì)象.
下面我們通過(guò)一個(gè)例子來(lái)體會(huì)一下這個(gè)過(guò)程:
我們先創(chuàng)建2個(gè)新的Coder對(duì)象:
Coder c1 = new Coder("bruce", 10); Coder c2 = new Coder("bruce", 10);
假定我們已經(jīng)重寫(xiě)了Coder的equals()方法而沒(méi)有重寫(xiě)hashCode()方法:
@Override public boolean equals(Object other) { System.out.println("equals method invoked!"); if(other == this) return true; if(!(other instanceof Coder)) return false; Coder o = (Coder)other; return o.name.equals(name) && o.age == age; }
然后我們構(gòu)造一個(gè)HashSet,將c1對(duì)象放入到set中:
Set<Coder> set = new HashSet<Coder>(); set.add(c1);
再執(zhí)行:
System.out.println(set.contains(c2));
我們期望contains(c2)方法返回true, 但實(shí)際上它返回了false.
c1和c2的name和age都是相同的,為什么我把c1放到HashSet中后,再調(diào)用contains(c2)卻返回false呢?這就是hashCode()在作怪了.因?yàn)槟銢](méi)有重寫(xiě)hashCode()方法,所以HashSet在查找c2時(shí),會(huì)在不同的bucket中查找.比如c1放到05這個(gè)bucket中了,在查找c2時(shí)卻在06這個(gè)bucket中找,這樣當(dāng)然找不到了.因此,我們重寫(xiě)hashCode()的目的在于,在A.equals(B)返回true的情況下,A, B 的hashCode()要返回相同的值.
我讓hashCode()每次都返回一個(gè)固定的數(shù)行嗎
有人可能會(huì)這樣重寫(xiě):
@Override public int hashCode() { return 10; }
如果這樣的話,HashMap, HashSet等集合類(lèi)就失去了其 "哈希的意義".用<Effective Java>中的話來(lái)說(shuō)就是,哈希表退化成了鏈表.如果hashCode()每次都返回相同的數(shù),那么所有的對(duì)象都會(huì)被放到同一個(gè)bucket中,每次執(zhí)行查找操作都會(huì)遍歷鏈表,這樣就完全失去了哈希的作用.所以我們最好還是提供一個(gè)健壯的hashCode()為妙.
總結(jié)
以上就是本文關(guān)于重寫(xiě)hashCode()和equals()方法詳細(xì)介紹的全部?jī)?nèi)容,希望對(duì)大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站其他相關(guān)專題,如有不足之處,歡迎留言指出。感謝朋友們對(duì)本站的支持!
- java中hashCode方法與equals方法的用法總結(jié)
- 詳解hashCode()和equals()的本質(zhì)區(qū)別和聯(lián)系
- JAVA hashCode使用方法詳解
- 詳解Java中用于查找對(duì)象哈希碼值的hashCode()函數(shù)
- 為什么在重寫(xiě) equals方法的同時(shí)必須重寫(xiě) hashcode方法
- java 中HashCode重復(fù)的可能性
- why在重寫(xiě)equals時(shí)還必須重寫(xiě)hashcode方法分享
- javascript中實(shí)現(xiàn)兼容JAVA的hashCode算法代碼分享
- 重新實(shí)現(xiàn)hashCode()方法
相關(guān)文章
IDEA工程運(yùn)行時(shí)總是報(bào)xx程序包不存在實(shí)際上包已導(dǎo)入(問(wèn)題分析及解決方案)
這篇文章主要介紹了IDEA工程運(yùn)行時(shí),總是報(bào)xx程序包不存在,實(shí)際上包已導(dǎo)入,本文給大家分享問(wèn)題分析及解決方案,通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2020-08-08SpringBoot實(shí)戰(zhàn)之實(shí)現(xiàn)結(jié)果的優(yōu)雅響應(yīng)案例詳解
這篇文章主要介紹了SpringBoot實(shí)戰(zhàn)之實(shí)現(xiàn)結(jié)果的優(yōu)雅響應(yīng)案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-09-09Java依賴-關(guān)聯(lián)-聚合-組合之間區(qū)別_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java依賴-關(guān)聯(lián)-聚合-組合之間區(qū)別理解,依賴關(guān)系比較好區(qū)分,它是耦合度最弱的一種,下文給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2017-08-08Java數(shù)據(jù)庫(kù)連接PreparedStatement的使用詳解
這篇文章主要介紹了Java數(shù)據(jù)庫(kù)連接PreparedStatement的使用詳解,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08IntelliJ IDEA 設(shè)置代碼提示或自動(dòng)補(bǔ)全的快捷鍵功能
這篇文章主要介紹了IntelliJ IDEA 設(shè)置代碼提示或自動(dòng)補(bǔ)全的快捷鍵功能,需要的朋友可以參考下2018-03-03