ITEM 11:重寫equals時也要復寫hashcode

ITEM 11:ALWAYS OVERRIDE HASHCODE WHEN YOU OVERRIDE EQUALS
??必須在每個重寫了 equals 方法的類中重寫 hashCode 方法急黎,如果不這樣做,您的類將違反 hashCode 的通用契約侧到,這可能導致它在 HashMap 和HashSet 這類集合中運行異常勃教。根據(jù) Object 規(guī)范,下面是具體的條款:

  • 當在程序執(zhí)行過程中反復調(diào)用對象的 hashCode 方法時匠抗,它必須始終返回相同的值(前提是在equals方法中不修改任何信息)故源,此值不需要在程序的一次執(zhí)行與另一次執(zhí)行之間保持一致。
  • 如果兩個對象 調(diào)用equals(Object) 方法汞贸,結果是相等的绳军,那么對這兩個對象調(diào)用hashCode 也必須返回相同的結果。
  • 如果兩個對象 調(diào)用equals(Object) 方法矢腻,結果不相等门驾,則不需要對每個對象調(diào)用hashCode 都必須產(chǎn)生不同的結果。但是多柑,程序員應該意識到奶是,為不同的對象生成不同的 hashcode 可以提高哈希表的性能。
    當您無法重寫 hashCode 方法時竣灌,違反的關鍵條款是第二個:相等的對象必須具有相等的 hashcode诫隅。類的equals方法的語義是,判斷兩個不同的實例在邏輯上是否是相等的帐偎,但是對于對象的 hashCode 方法來說逐纬,它們只是兩個沒有太多共同點的對象。因此削樊,Object 的 hashCode 方法可以返回兩個看似隨機的數(shù)字豁生,而不是契約要求的兩個相等的數(shù)字。
    ??例如漫贞,假設您嘗試使用 item 10 中的 PhoneNumber 類的實例作為 HashMap 中的鍵:
    Map<PhoneNumber, String> m = new HashMap<>(); m.put(new PhoneNumber(707, 867, 5309), "Jenny");
    ??此時甸箱,你也許希望 m.get(new PhoneNumber(707, 867, 5309)) 返回 "Jenny",但實際上代碼將返回null迅脐。注意芍殖,涉及兩個 PhoneNumber 實例:一個用于插入HashMap,另一個實例是我們檢索時新生成的谴蔑。PhoneNumber 類未能覆蓋hashCode 導致兩個相等的實例具有不同的散列碼豌骏,這違反了 hashCode 契約龟梦。因此,get方法可能會在與 put 方法命中的散列桶中查找窃躲。
    ??即使這兩個實例碰巧散列到同一個 bucket计贰,get 方法也幾乎肯定會返回 null,因為HashMap 有一個優(yōu)化蒂窒,可以緩存與每個條目關聯(lián)的散列碼躁倒,如果散列代碼不匹配,也不需要檢查對象是否相等洒琢。
    ??解決這個問題很簡單秧秉,只要為 PhoneNumber 編寫一個合適的 hashCode 方法就可以了。那么hashCode 方法應該是什么樣的呢衰抑?寫一個不符合規(guī)范的例子很簡單福贞,例如下面這條是合法的,但我們不應該使用:
// The worst possible legal hashCode implementation - never use!
@Override public int hashCode() { return 42; }

??這是合法的停士,因為它確保相等的對象具有相同的哈希碼挖帘。但它很糟糕,因為它讓所有對象都有相同的哈希碼恋技。因此拇舀,每個對象都散列到同一個桶中,散列表退化為鏈表蜻底。程序應該在 O(n) 時間運行骄崩,而不是在 O(n2) 運行。對于大型哈希表薄辅,可能導致程序無法工作要拂。
??一個好的哈希函數(shù)往往會為不相等的實例生成不同的哈希碼。這正是 hashCode 契約的第三部分的含義站楚。理想情況下脱惰,哈希函數(shù)應該在所有 int 值上均勻地分布任何合理的不相等實例集合。實現(xiàn)這一理想可能很困難窿春。幸運的是拉一,得到一個合理的近似并不難。這里有一個簡單的方法:

    1. 聲明一個名為 result 的 int 變量旧乞,并將其初始化為對象中第一個重要字段的哈希碼c蔚润,如步驟2.a中計算的那樣。(回顧 itme 10尺栖,一個重要字段是一個影響等號比較的字段嫡纠。)
    1. 對于對象中剩余的重要字段f,執(zhí)行以下操作:
      a.計算字段的int哈希碼c:
      i. 如果字段是基本類型,計算 Type.hashCode(f)除盏,其中type是與f的類型對應的裝箱基元類叉橱。
      ii. 如果字段是對象引用,并且該類的 equals 方法通過遞歸調(diào)用 equals 來比較字段痴颊,則遞歸調(diào)用字段上的 hashCode。如果需要更復雜的比較屡贺,請為該字段計算一個“規(guī)范表示”蠢棱,并在規(guī)范表示上調(diào)用hashCode。如果字段的值為null甩栈,則使用0 (或其他常量泻仙,但0是傳統(tǒng)的)。
      iii. 如果字段是一個數(shù)組量没,則將其中每個重要元素視為一個單獨的字段玉转。也就是說,通過遞歸地應用這些規(guī)則殴蹄,為每個重要元素計算一個哈希碼究抓,并在每個步驟2.b中組合這些值。如果數(shù)組沒有重要元素袭灯,則使用常量刺下,最好不是0。如果所有的元素都是重要的稽荧,使用Arrays.hashCode橘茉。
      b.結合步驟2中計算的哈希碼c。a轉化結果如下:
      result = 31 * result + c;
    1. Return result.
      ??當您完成 hashCode 方法的編寫時姨丈,請自問 equal 實例是否具有相同的散列代碼畅卓。編寫單元測試來驗證您的直覺(除非您使用AutoValue來生成equals和hashCode方法,在這種情況下蟋恬,您可以安全地忽略這些測試)翁潘。如果相等的實例有不相等的哈希碼,找出原因并解決問題歼争。
      ??可以在哈希碼計算中排除派生字段唐础。換句話說,您可以忽略任何字段矾飞,其值可以從計算中包含的字段計算出來一膨。
      ??您必須排除在equals比較中沒有使用的任何字段,否則您可能會違反 hashCode 契約的第二項規(guī)定洒沦。
      ??2.b 中的乘法使結果依賴于字段的順序豹绪,如果類有多個類似的字段,則生成更好的散列函數(shù)。例如瞒津,如果從字符串哈希函數(shù)中省略乘法蝉衣,所有的字謎都將具有相同的哈希代碼。之所以選擇31巷蚪,是因為它是一個質(zhì)數(shù)病毡。如果它是偶數(shù),并且乘法溢出屁柏,信息就會丟失啦膜,因為乘以2等于移位。使用質(zhì)數(shù)的優(yōu)勢不太明顯淌喻,但它是傳統(tǒng)的僧家。31的一個很好的特性是,可以用移位和減法替換乘法裸删,從而在某些架構上獲得更好的性能:
      31 * i == (i << 5) - i
      ??讓我們將之前的方法應用到PhoneNumber類:
// Typical hashCode method
@Override public int hashCode() {
  int result = Short.hashCode(areaCode);
  result = 31 * result + Short.hashCode(prefix); 
  result = 31 * result + Short.hashCode(lineNum); 
  return result;
}

??由于該方法返回一個簡單確定性計算的結果八拱,該計算的唯一輸入是PhoneNumber實例中的三個重要字段。顯然涯塔,相同的PhoneNumber實例具有相同的哈希碼肌稻。實際上,這個方法是 PhoneNumber 的一個非常好的 hashCode 實現(xiàn)匕荸,可以與Java平臺庫中的實現(xiàn)相媲美灯萍。它很簡單,速度也相當快每聪,并且能夠?qū)⒉幌嗟鹊碾娫捥柎a分散到不同的散列桶中旦棉。
??雖然上面的例子是一個相當好的散列函數(shù),但它們并不是最棒的药薯。它們在質(zhì)量上可以與Java平臺庫的值類型中的散列函數(shù)相媲美绑洛,并且對于大多數(shù)用途都是足夠的。如果您確實需要不太可能產(chǎn)生沖突的散列函數(shù)童本,請參閱Guava的com.google.common.hash.Hashing [Guava]真屯。
??Objects類有一個靜態(tài)方法,它接受任意數(shù)量的對象并為它們返回一個散列代碼穷娱。這個名為 hash 的方法允許您編寫單行 hashCode 方法绑蔫,其質(zhì)量可與根據(jù)該項目中的配方編寫的方法相媲美。不幸的是泵额,它們運行得更慢配深,因為它們需要創(chuàng)建數(shù)組來傳遞可變數(shù)量的參數(shù),如果其中任何參數(shù)是原始類型嫁盲,則需要裝箱和解箱篓叶。建議只在性能不重要的情況下使用這種類型的哈希函數(shù)。下面是一個使用這種技術編寫的PhoneNumber 函數(shù):

// One-line hashCode method - mediocre performance
@Override public int hashCode() {
  return Objects.hash(lineNum, prefix, areaCode);
}

??如果一個類是不可變的,并且計算散列代碼的成本很高缸托,那么您可以考慮將散列代碼緩存到對象中左敌,而不是每次請求時重新計算它。如果您認為這種類型的大多數(shù)對象都將用作散列鍵俐镐,那么您應該在創(chuàng)建實例時計算散列代碼矫限。否則,您可能選擇在第一次調(diào)用散列代碼時惰性地初始化散列代碼佩抹。需要注意叼风,確保類在存在延遲初始化的字段時仍然是線程安全的。我們的 PhoneNumber 類不需要這種處理匹摇,但只是向您展示它是如何完成的咬扇,在這里甲葬。注意廊勃,hashCode字段的初始值(在本例中為0)不應該是通常創(chuàng)建的實例的哈希碼:

// hashCode method with lazily initialized cached hash code private int hashCode; 
// Automatically initialized to 0
@Override public int hashCode() { 
  int result = hashCode;
  if (result == 0) {
    result = Short.hashCode(areaCode);
    result = 31 * result + Short.hashCode(prefix); 
    result = 31 * result + Short.hashCode(lineNum); 
    hashCode = result;
  }
  return result; 
}

??不要試圖從哈希代碼計算中排除重要字段來提高性能。雖然生成的哈希函數(shù)可能運行得更快经窖,但其質(zhì)量差可能會降低哈希表的性能坡垫,使其無法使用。特別是画侣,哈希函數(shù)可能會遇到大量實例冰悠,這些實例主要在您選擇忽略的區(qū)域中不同。如果發(fā)生這種情況配乱,哈希函數(shù)將把所有這些實例映射到幾個哈希代碼溉卓,應該在線性時間內(nèi)運行的程序?qū)⒃诙螘r間內(nèi)運行。
??這不僅僅是一個理論問題搬泥。在Java 2之前桑寨,字符串哈希函數(shù)最多使用16個字符,以第一個字符開始忿檩,在整個字符串中平均間隔尉尾。對于層次結構名稱的大集合(如url),此函數(shù)完全顯示了前面描述的病態(tài)行為燥透。
??不要為 hashCode 返回的值提供詳細的規(guī)范沙咏,因此客戶端不能合理地依賴它;這使您可以靈活地更改它班套。Java庫中的許多類肢藐,如 String 和 Integer,都指定它們的hashCode 方法返回的確切值作為實例值的函數(shù)吱韭。這不是一個好主意窖壕,而是我們不得不面對的一個錯誤:它阻礙了在未來版本中改進 hash 函數(shù)的能力。如果您沒有指定細節(jié),并且在散列函數(shù)中發(fā)現(xiàn)了一個缺陷瞻讽,或者發(fā)現(xiàn)了一個更好的散列函數(shù)鸳吸,您可以在后續(xù)版本中修改它。
??總之速勇,每次重寫 equals 時都必須重寫 hashCode晌砾,否則程序?qū)o法正確運行。您的hashCode 方法必須遵守對象中指定的通用契約烦磁,并且必須合理地為不相等的實例分配不相等的哈希碼养匈。使用本文提供的規(guī)范,這很容易實現(xiàn)都伪,盡管有點乏味呕乎。正如item 10 中提到的,AutoValue 框架提供了一個很好的替代方法陨晶,可以替代手工編寫 equals 和hashCode 方法猬仁,IDE也提供了一些功能。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末先誉,一起剝皮案震驚了整個濱河市湿刽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌褐耳,老刑警劉巖贸宏,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痴柔,死亡現(xiàn)場離奇詭異苍糠,居然都是意外死亡左刽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門刃滓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仁烹,“玉大人,你說我怎么就攤上這事注盈』挝#” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵老客,是天一觀的道長僚饭。 經(jīng)常有香客問我,道長胧砰,這世上最難降的妖魔是什么鳍鸵? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮尉间,結果婚禮上偿乖,老公的妹妹穿的比我還像新娘击罪。我一直安慰自己,他們只是感情好贪薪,可當我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布媳禁。 她就那樣靜靜地躺著,像睡著了一般画切。 火紅的嫁衣襯著肌膚如雪竣稽。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天霍弹,我揣著相機與錄音毫别,去河邊找鬼。 笑死典格,一個胖子當著我的面吹牛岛宦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播耍缴,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼砾肺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了私恬?” 一聲冷哼從身側響起债沮,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤炼吴,失蹤者是張志新(化名)和其女友劉穎本鸣,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體硅蹦,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡荣德,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了童芹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涮瞻。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖假褪,靈堂內(nèi)的尸體忽然破棺而出署咽,到底是詐尸還是另有隱情,我是刑警寧澤生音,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布宁否,位于F島的核電站,受9級特大地震影響缀遍,放射性物質(zhì)發(fā)生泄漏慕匠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一域醇、第九天 我趴在偏房一處隱蔽的房頂上張望台谊。 院中可真熱鬧蓉媳,春花似錦、人聲如沸锅铅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盐须。三九已至号杠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間丰歌,已是汗流浹背姨蟋。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留立帖,地道東北人眼溶。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像晓勇,于是被迫代替她去往敵國和親堂飞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 孤獨(1) 不要以為我說這兩個字就以為我真的孤獨 我從來不怕孤獨 孤獨是我的孿生姊妹 我喜歡與她...
    簡書作者木瓜閱讀 192評論 0 2
  • 放假了,真好描融,早晨可以睡到自然醒铝噩,也可以悠哉悠哉的去晨練,仔細的關注一下公園里的花草樹木窿克,不用眼巴巴的看著...
    詩和遠方昕閱讀 183評論 0 2
  • 30歲,放棄公司副總的職位只损,重新從零創(chuàng)業(yè)一姿,你敢嗎? 40歲跃惫,并且有一個17歲的兒子叮叹,現(xiàn)在再生一個小baby,你敢嗎...
    林洢閱讀 668評論 0 1