Effective Java(3rd)-Item11 重寫equals時(shí)始終重寫hashCode

??你必須在每個(gè)類中重寫hashCode,只要你重寫了equals。如果你不這么做,你的類將違反了hashCode的一般約定,這將阻止它在HashMap和hashSet這樣的集合中正常運(yùn)作。如下是改編自對(duì)象規(guī)范的約定:

  • 當(dāng)在應(yīng)用程序執(zhí)行期間對(duì)對(duì)象重復(fù)調(diào)用 hashCode 方法時(shí),它必須一致地返回相同的值,前提是不對(duì) equals 比較中使用的信息進(jìn)行修改。在應(yīng)用程序不同的執(zhí)行期間,這個(gè)值不需要一直保持一致。
  • 如果兩個(gè)對(duì)象根據(jù)通過equals(Object)方法相等,則在這兩個(gè)對(duì)象調(diào)用hashCode時(shí),必須生成相同的整數(shù)結(jié)果。
  • 如果兩個(gè)對(duì)象根據(jù)equals(Object)方法得出不相等,則不需要在每個(gè)對(duì)象上調(diào)用hashCode必須不相同。然而,程序員應(yīng)該意識(shí)到,為不相同對(duì)象生成不相同的結(jié)果可能會(huì)提高h(yuǎn)ash表的性能。
    ??當(dāng)你不能重寫hashCode時(shí),就違反了第二個(gè)關(guān)鍵條約:相等的對(duì)象必須要有相等的hashCode。兩個(gè)不同的實(shí)例可能因?yàn)轭惖膃quals方法在邏輯上相等,但是對(duì)于Object的hashCode來說,它們只是兩個(gè)沒有多少共同點(diǎn)的對(duì)象。因此,Object的hashCode方法返回了兩個(gè)看似隨機(jī)的數(shù)字,而不是條約要求的兩個(gè)相等的數(shù)字。
    ??例如,假設(shè)你嘗試使用item10中的PhoneNumber類的實(shí)例作為HashMap的鍵:
    image.png

??此時(shí),你可能期望m.get(new PhoneNumber(707, 867, 5309)) 返回“Jenny”,但是相反,它返回了null。注意到涉及到兩個(gè)PhoneNumber的實(shí)例:一個(gè)用于插入HashMap,另一個(gè)相等的實(shí)例用于(嘗試)檢索。PhoneNumber類未能重寫hashCode造成了兩個(gè)相等實(shí)例具有不相等的hashCode,違反了hashCode約定。因此,get方法可能與put方法存儲(chǔ)的在不同的hash桶中查詢phoneNumber。即使兩個(gè)實(shí)例碰巧hash到了同一個(gè)桶,get方法機(jī)會(huì)肯定會(huì)返回null,因?yàn)镠ashMap有一個(gè)優(yōu)化,緩存了每個(gè)條目相關(guān)的hashCode,如果hashCode沒有,則不需要檢查對(duì)象是否相等。
??修復(fù)這個(gè)問題就像為PhoneNumber編寫一個(gè)合適的hashCode意義簡(jiǎn)單。那么hashCode應(yīng)該長(zhǎng)什么樣呢?很容易寫好一個(gè)糟糕的例子。例如,這個(gè)代碼總是合法的,但是永遠(yuǎn)不該被使用:


image.png

??這個(gè)合法的,因?yàn)樗_保了相等的對(duì)象擁有相同的hashCode。這是糟糕的,因?yàn)樗_保了每個(gè)對(duì)象都有了相同的hashCode。因此,每個(gè)對(duì)象散列到了相同的桶中,哈希表退化到了linked list。程序本應(yīng)以線性時(shí)間運(yùn)行,但是以二次方時(shí)間運(yùn)行了。對(duì)于大型哈希表,這是工作與不工作的區(qū)別。
??一個(gè)好的hash函數(shù)往往為不相等的實(shí)例生成不相等的hashCode。這恰恰是hashCode約定第三條的意義。理想情況下, hash 函數(shù)應(yīng)該在所有 int 值之間均勻分布所有不相等實(shí)例的合理集合。實(shí)現(xiàn)這個(gè)想法可能非常困難。幸運(yùn)的是,實(shí)現(xiàn)一個(gè)類似的集合并不太難。如下是一個(gè)簡(jiǎn)單的菜譜:
1.聲明一個(gè)int變量叫做result,初始化它為hashCode c作為你的對(duì)象的第一個(gè)重要字段,如步驟2.a計(jì)算的那樣(回顧item10,重要字段影響euqals的比較)。
2.對(duì)于你的對(duì)象中每個(gè)剩下的重要字段f,執(zhí)行如下操作:
??a.計(jì)算字段的int hashCode c:
????i.如果字段是基本類型,計(jì)算Type.hashCode(f),Type是與f類型對(duì)于的包裝類型。
????ii.如果字段是一個(gè)對(duì)象引用,并且這個(gè)類的equals方法通過遞歸調(diào)用equals來比較該字段,則在該字段上遞歸調(diào)用hashCode。如果需要更復(fù)雜的比較,則為這個(gè)字段計(jì)算“規(guī)范表示”并在規(guī)范表示上調(diào)用hashCode。如果字段的值是null,使用0(或其他常量,但0是傳統(tǒng))。
????iii.如果字段是數(shù)組,視其每個(gè)重要元素都為獨(dú)立的字段。也就是說,通過遞歸地調(diào)用準(zhǔn)備些規(guī)則為每個(gè)重要的元素計(jì)算hashCode,組合每個(gè)步驟的2.b。如果數(shù)組沒有重要元素,使用一個(gè)常量,最好不要是0.如果所有元素都是重要的,使用Arrays.hashCode.
??b.聯(lián)合在step2.a計(jì)算的hashCode c組合到result中,如下所示:
???? result = 31 * result + c;
3.返回result。
??當(dāng)你完成編寫hashCode方法時(shí),問問你自己是否相等的實(shí)例有著相等的hashCode.編寫單元測(cè)試來證實(shí)你的直覺(除非你使用AutoValue來生成你的equals和hashCode方法,在這種情況下你可以安全地省略這些測(cè)試)。如果相等實(shí)例有著不相等的hashCode,指出原因并修復(fù)這些問題。
??你可以從hashCode計(jì)算中排除派生字段。換句話說,你可以忽略任何可以從計(jì)算中包含的字段計(jì)算其值的字段。你必須排除不在equals比較使用的任何字段。否則你就有違反hashCode第二條約定的風(fēng)險(xiǎn)。
??步驟2.b的乘法使得結(jié)果取決于字段的順序,如果類有多個(gè)相似字段,就能產(chǎn)生一個(gè)更好的哈希函數(shù)。例如,如果在String哈希函數(shù)中忽略了乘法,所有“anagrams”(譯者注:
比如 “abc” "acb" "cba" 如果沒有乘法,這三個(gè)String的hashCode必定一致)具有相同的hashCode。選擇31是因?yàn)樗且粋€(gè)奇數(shù)質(zhì)數(shù)。如果它是偶數(shù)且乘法溢出,信息將被丟失,因?yàn)槌?等同于移位。使用素?cái)?shù)的優(yōu)勢(shì)不太明確,但是它是傳統(tǒng)的。31的一個(gè)很好的特性是乘法可以被以為和減法代替,以便在一些架構(gòu)上獲得更好的性能: 31 * i == (i << 5) - i?,F(xiàn)在VM自動(dòng)執(zhí)行這類優(yōu)化。
??讓我們將這些配方應(yīng)用在PhoneNumber類中:

image.png

??因?yàn)檫@個(gè)方法返回了一個(gè)簡(jiǎn)單確定性計(jì)算的結(jié)果,其輸入僅為一個(gè)PhoneNumber實(shí)例中的三個(gè)重要字段,顯然相同PhoneNumber有著相同的hashCode。事實(shí)上,這個(gè)方法對(duì)于PhoneNumber來說是一個(gè)非常好的hashCode實(shí)現(xiàn),與Java平臺(tái)庫(kù)中的方法相同。它很簡(jiǎn)單,速度相當(dāng)快,可以合理地分散不相等的phone numbers到不同的哈希桶中。
??雖然這個(gè)項(xiàng)目中的配方產(chǎn)生了相當(dāng)好的哈希函數(shù),但是它們不是最先進(jìn)的。它們的質(zhì)量與Java平臺(tái)庫(kù)的值類型中的哈希函數(shù)相當(dāng),并且適用于大多數(shù)用途。如果你真正需要哈希函數(shù)不太可能產(chǎn)生沖突,查看 Guava’s com.google.common.hash.Hashing [Guava]。
??Objects類有一個(gè)靜態(tài)方法,接受一個(gè)任意數(shù)量的對(duì)象并且返回它們的hashCode。這個(gè)方法的名字叫做”hash“。允許你編寫單行hashCode方法,其質(zhì)量與根據(jù)本條目中的配方編寫的方法相當(dāng)。不幸的是,它們運(yùn)行地更慢因?yàn)樗鼈冃枰獢?shù)組創(chuàng)建來傳遞可變數(shù)量地參數(shù),以及裝箱和取消裝箱,如果參數(shù)有原始類型的話。建議僅在性能不那么重要的情況下使用這個(gè)形式的哈希函數(shù)。這是使用這種技術(shù)編寫的PhoneNumber的哈希函數(shù):


image.png

??如果類是不可變的并且計(jì)算hashCode的開銷是巨大的,你可能需要考慮在對(duì)象內(nèi)緩存hashCode而不是在每次請(qǐng)求時(shí)重新計(jì)算。如果你認(rèn)為這個(gè)類型的大多數(shù)對(duì)象將用作hash key,你應(yīng)該在實(shí)例被創(chuàng)建的時(shí)候計(jì)算hashCode。否則,你可能在你第一次調(diào)用hashCode時(shí)選擇懶初始化hashCode。需要注意確保存在懶初始化字段時(shí)保證線程安全 (item83) 。我們的PhoneNumber類不值得這么處理,只是想你展示它是怎么完成的,如你所見。注意hashCode初始化值不該是常用實(shí)例
的哈希碼(本例中是0):

image.png

??不要試圖在哈希碼計(jì)算時(shí)排除重要字段來提高性能。雖然生成的哈希函數(shù)可能會(huì)運(yùn)行更快,但是它的低質(zhì)量可能回事哈希表的性能降低到不能使用的程度。特別地,哈希函數(shù)可以面對(duì)大量實(shí)例的集合,主要區(qū)別在于你的選擇性忽略的區(qū)域。如果這種情況發(fā)生了,哈希函數(shù)將映射所有的實(shí)例到一小部分hashCode,本該在線性時(shí)間運(yùn)行的程序?qū)⒉坏貌皇褂胣^2的時(shí)間。
??這不僅僅只是一個(gè)理論問題。在Java2之前,String的哈希函數(shù)在整個(gè)字符串中均勻分布最多16個(gè)字符,從第一個(gè)字符開始。對(duì)于大型分層名稱集合,比如URL,這個(gè)方法完全顯示前面描述的病態(tài)行為。
??不要為hashCode返回的值提供詳細(xì)的規(guī)范,因?yàn)檫@樣客戶端就不能合理地依賴它;這可以是你靈活地改變它。許多在Java庫(kù)中的類,比如String和Integer,將hashCode方法返回的確切值指定為實(shí)例值得函數(shù)。這不是一個(gè)好主意,但是這是一個(gè)我們不得不忍受的錯(cuò)誤:它阻礙了在未來版本中改進(jìn)哈希函數(shù)的能力。如果未指定詳細(xì)信息并且在散列函數(shù)中發(fā)現(xiàn)缺陷或發(fā)現(xiàn)更好的散列函數(shù),你就可以在后續(xù)版本中改變它。
??總之,每次覆寫equals方法時(shí)你必須覆寫hashCode,否則你的程序?qū)o法運(yùn)行正確。你的hashCode方法必須遵守Object中指定的常規(guī)規(guī)則,并且必須合理地將不相等的hashCode分配給不相等的實(shí)例。只要使用51頁(yè)的配方,這就很容易實(shí)現(xiàn)。在item10提到的AutValue框架,提供了自動(dòng)編寫equals和hashCode方法的優(yōu)秀替代方案,IDE也提供了一些功能。
本文寫于2019.3.2,歷時(shí)5天

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • [{"reportDate": "2018-01-23 23:28:49","fluctuateCause": n...
    加勒比海帶_4bbc閱讀 892評(píng)論 1 2
  • 目錄: Android:Android 0.*Android 1.*Android 2.*Android 3.*A...
    敲代碼的令狐蔥閱讀 4,519評(píng)論 0 2
  • 旅途的心情,就是聽著淡淡的音樂,看著陌生的風(fēng)景陌生的臉!然后開啟漫想模式。
    一片彬心閱讀 159評(píng)論 0 0
  • 這是我看得最快的一本書,用了四天時(shí)間。 這樣的書絕不是我會(huì)主動(dòng)選擇去看的風(fēng)格。之所以會(huì)讀到這樣的文字,要感謝一位偉...
    叫我美琳閱讀 150評(píng)論 0 0
  • 你有沒有一種體驗(yàn),當(dāng)期待一件事情發(fā)生的時(shí)候,頭腦中不停的想象那個(gè)實(shí)現(xiàn)的畫面,越想越美好。而等到真正實(shí)現(xiàn)的那天,卻覺...
    曹小力閱讀 2,892評(píng)論 5 1

友情鏈接更多精彩內(nèi)容