天天看點

黑馬程式員------Java深度之—關于 hashCode() 你需要了解的 3 件事

------<a href="http://www.itheima.com" target="blank">Java教育訓練、Android教育訓練、iOS教育訓練、.Net教育訓練</a>、期待與您交流! -------

------<a href="http://www.itheima.com" target="blank">Java教育訓練、Android教育訓練、iOS教育訓練、.Net教育訓練</a>、期待與您交流! -------

在 Java 中,每一個對象都有一個容易了解但是仍然有時候被遺忘或者被誤用的 hashCode 方法。這裡有3件事情要時刻牢記以避免常見的陷阱。

一個對象的哈希碼允許算法和資料結構将對象放入隔間,就象列印機類型案件中的字母類型。列印機将所有的“A”類型放到一個房間,它尋找這個“A”的時候就隻需要在這個房間進行尋找。這種簡單的系統讓他在未排序的抽屜中尋找類型的時候更快。這也是基于哈希的集合的想法,例如 HashMap 和 HashSet。

為了使你的類與其他基于哈希的集合或其他依賴哈希碼的算法一起正常工作,所有 hashCode 的實作必須遵守一個簡單的契約。

hashCode 契約

這個契約在 hashCode 方法的 JavaDoc 中進行了闡述。它可以大緻的歸納為下面幾點:

  1. 在一個運作的程序中,相等的對象必須要有相同的哈希碼
  2. 請注意這并不意味着以下常見的誤解:
  3. 不相等的對象一定有着不同的哈希碼——錯!
  4. 有同一個哈希值的對象一定相等——錯!
黑馬程式員------Java深度之—關于 hashCode() 你需要了解的 3 件事

這個契約允許不同的對象共享相同的哈希碼,例如根據上圖中的的描述,“A”和“μ”對象的哈希值就一樣。在數學術語中,從對象到哈希碼的映射不一定為内射或者雙射。這是顯而易見的,因為可能的不同對象的數量經常比可能的哈希嗎的數量 (2^32)更大。

編輯:在早期的版本中,我錯誤的認為哈希碼的映射一定屬于内射,但是不一定是雙射,這顯然是錯的。感謝 Lucian 指出這個錯誤。

這個約定直接導緻了第一個規則:

1. 無論你何時實作 equals 方法,你必須同時實作 hashCode 方法

如果你不這樣做,你将會帶來損壞的對象。為什麼?一個對象的 hashCode 方法需要與 equals 方法考慮同樣的域。通過重寫 equals 方法,你将申明一些對象與其他對象相等,但是原始的 hashCode 方法将所有的對象看做是不同的。是以你将會有不同哈希碼的相同對象。例如,在 HashMap 中調用 contains 方法将會傳回 false,即使這個對象已經被添加。

怎樣寫一個好的 hashCode 方法不在這篇文章的範圍内,在 Joshua Bloch 很受歡迎的書《Effective Java》中被很好的闡釋,Java 開發人員的書架上不應缺少這本書。

【你的項目需要專業意見嗎?我們的 Developer Support 會為你解決問題。|在我們的 Software Craftsmanship 頁面上尋找關于怎樣編寫簡潔代碼的更多提示。】

為了安全起見,讓 Eclipse IDE 一次産生 equals 和 hashCode 方法: Source > Generate hashCode() and equals()….

黑馬程式員------Java深度之—關于 hashCode() 你需要了解的 3 件事

為了保護你自己,你還可以配置 Eclipse 來檢測實作了 equals 方法但是沒有實作 hashCode 方法的類,并顯示錯誤。不幸的是,此選項預設是指為“忽略”:Preferences > Java >Compiler > Errors/Warnings,然後用快速篩選器來搜尋“hashcode”:

黑馬程式員------Java深度之—關于 hashCode() 你需要了解的 3 件事

更新:正如 laurent 指出,equalsverifier 是一個強大的工具,它用來驗證 hashCode 和 equals 方法的約定。您應該考慮在您的單元測試中使用它。

哈希碼沖突

任何時候,兩個不同對象有相同的哈希碼,我們稱之為沖突。沖突不要緊,它隻是意味着有多個對象在同一個空間裡,是以 HashMap 會再檢查一遍來找正确的對象。大量的沖突将會降低系統的性能,但是它們不會導緻錯誤的結果。

但是如果你誤認為哈希碼是一個對象唯一的句柄,例如使用它作為Map的key,你有時會得到錯誤的對象。因為雖然沖突很罕見,但他們是不可避免的。例如,字元“Aa”和“BB”産生相同的哈希碼:2112。是以:

2. 永遠不要把哈希碼誤用作一個key

你可能會反對,不像列印機的類型例子,在 Java 中,有 4,294,967,296 的空間(2^32 個可能的整型值)。40億的插槽,發生沖突似乎是極不可能的對嗎?

黑馬程式員------Java深度之—關于 hashCode() 你需要了解的 3 件事

事實證明它不是不太可能。這是令人驚訝的沖突:請想象一下在一個房間裡有 23 個随機的人。你覺得兩個人是同一天生日的幾率有多大 ?很低,因為一年有 365 天嗎?事實上,幾率是 50% 左右!50 個人是保守的估計。這個現象稱為生日悖論。應用到哈希碼,這意味着在 77163 個不同的對象中,有 50% 的可能性發生沖突–假設你有一個理想的哈希的函數,均勻地把對象分布在所有可用的空間裡面。

例如:

安然公司的電子郵件集包含 520,924 封電子郵件。計算電子郵件内容字元串的哈希碼時,我發現 50 對(甚至是 2 個三元組)不同的電子郵件有着相同的哈希碼。對于五十萬個字元串,這是一個很好的結果。但是這裡的資訊是:如果你有很多資料元素,沖突就會發生。如果你正在使用哈希碼作為 key,你不會立即注意到你的錯誤。但是少數人會收到錯誤的郵件。

哈希碼可變

最後,在哈希碼的契約中,有一個很重要的細節是相當讓人吃驚的:hashCode 并不保證在不同的應用執行中得到相同的結果。讓我們看一看 Java 文檔:

在一次 Java 應用的執行中,對于同一個對象,hashCode 方法必須始終傳回相同的整數,但這整數不反映對象是否被修改(equals 比較)的資訊。同一個應用的不同執行,該整數不必保持一緻。

事實上,這是不常見的,一些類庫中的類甚至指定它們用于計算哈希碼的精确公式(例如字元串)。對于這些類,哈希碼總是會相同。雖然大部分的哈希碼的實作提供穩定的值,但你不能依賴于這一點。正如這篇文章指出的,有些類庫在不同程序中會傳回不同的哈希值,這有的時候會讓人困惑。谷歌的 Protocol Buffers 就是一個例子。

是以,你不應該在分布式應用程式中使用哈希碼。一個遠端對象可能與本地對象有不同的哈希碼,即使這兩個對象是相等的。

3. 在分布式應用中不要使用哈希碼

此外,你應該意識到從一個版本到另一個版本哈希碼的功能實作可能會更改。是以您的代碼不應該依賴于任何特定的哈希碼值。例如,你不應該使用哈希碼來持久化狀态。下次你運作程式的時候,“相同”對象的哈希碼可能不同。

最好的建議可能是:完全不使用哈希碼,除非你自己創造了基于哈希的算法。

一種替代方法:SHA1

你可能知道加密的哈希碼 SHA1 有時被用來辨別對象(例如,git這樣做)。這也是不安全嗎?不。SHA1 使用 160 位密鑰,這使得沖突幾乎是不可能的。即使有很多對象,在這個空間發生沖突的幾率遠遠低于一顆流星撞到你正在執行程式的電腦的幾率。這篇文章對沖突的機率作了很好的概述。

關于哈希碼應該還有其他可談的,但這些看起來是最重要的。如果我有什麼遺漏,歡迎告訴我。

繼續閱讀