天天看點

HashMap 的工作原理

HashMap 的工作原理是近年來常見的 Java 面試題。幾乎每個 Java 程式員都知道 HashMap,都知道哪裡要用 HashMap,知道 Hashtable和HashMap之間的差別 ,那麼為何這道面試題如此特殊呢?是因為這道題考察的深度很深。這題經常出現在進階或中進階面試中。投資銀行更喜歡問這個問題,甚至會要求你實作 HashMap 來考察你的程式設計能力。

ConcurrentHashMap

和其它同步集合的引入讓這道題變得更加複雜。讓我們開始探索的旅程吧!

先來些簡單的問題

“你用過 HashMap 嗎?” “什麼是 HashMap?你為什麼用到它?”

幾乎每個人都會回答"是的”,然後回答 HashMap 的一些特性,譬如 HashMap 可以接受

null

鍵值和值,而

Hashtable

則不能;HashMap 是非

synchronized

; HashMap 很快;以及HashMap 儲存的是鍵值對等等。這顯示出你已經用過 HashMap ,而且對它相當的熟悉。但是面試官來個急轉直下,從此刻開始問出一些刁鑽的問題,關于 HashMap 的更多基礎的細節。面試官可能會問出下面的問題:

“你知道 HashMap 的工作原理嗎?” “你知道 HashMap 的

get()

方法的工作原理嗎?”

你也許會回答“我沒有詳查标準的Java API,你可以看看Java源代碼或者Open JDK。”“我可以用Google找到答案。”

但一些面試者可能可以給出答案,“ HashMap 是基于

hashing

的原理,我們使用

put(key, value)

存儲對象到HashMap中,使用

get(key)

從 HashMap 中擷取對象。當我們給

put()

方法傳遞鍵和值時,我們先對鍵調用

hashCode()

方法,傳回的

hashCode

用于找到

bucket

位置來儲存

Entry

對象。”這裡關鍵點在于指出, HashMap 是在

bucket

中儲存鍵對象和值對象,作為

Map.Entry

。這一點有助于了解擷取對象的邏輯。如果你沒有意識到這一點,或者錯誤的認為僅僅隻在

bucket

中存儲值的話,你将不會回答如何從 HashMap 中擷取對象的邏輯。這個答案相當的正确,也顯示出面試者确實知道hashing以及HashMap的工作原理。但是這僅僅是故事的開始,當面試官加入一些Java程式員每天要碰到的實際場景的時候,錯誤的答案頻現。下個問題可能是關于HashMap中的碰撞探測(collision detection)以及碰撞的解決方法:

“當兩個對象的

hashcode

相同會發生什麼?” 從這裡開始,真正的困惑開始了,一些面試者會回答因為

hashcode

相同,是以兩個對象是相等的,HashMap 将會抛出異常,或者不會存儲它們。然後面試官可能會提醒他們有

equals()

hashCode()

兩個方法,并告訴他們兩個對象就算

hashcode

相同,但是它們可能并不相等。一些面試者可能就此放棄,而另外一些還能繼續挺進,他們回答“因為 hashcode 相同,是以它們的

bucket

位置相同,‘碰撞’會發生。因為HashMap使用連結清單存儲對象,這個

Entry

(包含有鍵值對的

Map.Entry

對象)會存儲在連結清單中。”這個答案非常的合理,雖然有很多種處理碰撞的方法,這種方法是最簡單的,也正是 HashMap 的處理方法。但故事還沒有完結,面試官會繼續問:

“如果兩個鍵的

hashcode

相同,你如何擷取值對象?” 面試者會回答:當我們調用

get()

方法,HashMap會使用鍵對象的

hashcode

找到

bucket

位置,然後擷取值對象。面試官提醒他如果有兩個值對象儲存在同一個

bucket

,他給出答案:将會周遊連結清單直到找到值對象。面試官會問因為你并沒有值對象去比較,你是如何确定确定找到值對象的?除非面試者直到HashMap在連結清單中存儲的是鍵值對,否則他們不可能回答出這一題。

其中一些記得這個重要知識點的面試者會說,找到

bucket

位置之後,會調用

keys.equals()

方法去找到連結清單中正确的節點,最終找到要找的值對象。完美的答案!

許多情況下,面試者會在這個環節中出錯,因為他們混淆了

hashCode()

equals()

方法。因為在此之前

hashCode()

屢屢出現,而

equals()

方法僅僅在擷取值對象的時候才出現。一些優秀的開發者會指出使用不可變的、聲明作

final

的對象,并且采用合适的

equals()

hashCode()

方法的話,将會減少碰撞的發生,提高效率。不可變性使得能夠緩存不同鍵的hashcode,這将提高整個擷取對象的速度,使用

String

Interger

這樣的

wrapper

類作為鍵是非常好的選擇。

如果你認為到這裡已經完結了,那麼聽到下面這個問題的時候,你會大吃一驚。“如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麼辦?”除非你真正知道HashMap的工作原理,否則你将回答不出這道題。預設的負載因子大小為

0.75

,也就是說,當一個map填滿了75%的

bucket

時候,和其它集合類(如ArrayList等)一樣,将會建立原來HashMap大小的兩倍的

bucket

數組,來重新調整map的大小,并将原來的對象放入新的

bucket

數組中。這個過程叫作

rehashing

,因為它調用hash方法找到新的bucket位置。

如果你能夠回答這道問題,下面的問題來了:“你了解重新調整HashMap大小存在什麼問題嗎?”你可能回答不上來,這時面試官會提醒你當多線程的情況下,可能産生條件競争(race condition)。

當重新調整HashMap大小的時候,确實存在條件競争,因為如果兩個線程都發現HashMap需要重新調整大小了,它們會同時試着調整大小。在調整大小的過程中,存儲在連結清單中的元素的次序會反過來,因為移動到新的

bucket

位置的時候,HashMap并不會将元素放在連結清單的尾部,而是放在頭部,這是為了避免尾部周遊(tail traversing)。如果條件競争發生了,那麼就死循環了。這個時候,你可以質問面試官,為什麼這麼奇怪,要在多線程的環境下使用HashMap呢?:)

熱心的讀者貢獻了更多的關于HashMap的問題:

為什麼String, Interger這樣的wrapper類适合作為鍵? String, Interger這樣的

wrapper

類作為HashMap的鍵是再适合不過了,而且String最為常用。因為String是不可變的,也是

final

的,而且已經重寫了

equals()

hashCode()

方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算

hashCode()

,就要防止鍵值改變,如果鍵值在放入時和擷取時傳回不同的

hashcode

的話,那麼就不能從 HashMap中找到你想要的對象。不可變性還有其他的優點如線程安全。如果你可以僅僅通過将某個

field

聲明成

final

就能保證

hashCode

是不變的,那麼請這麼做吧。因為擷取對象的時候要用到

equals()

hashCode()

方法,那麼鍵對象正确的重寫這兩個方法是非常重要的。如果兩個不相等的對象傳回不同的

hashcode

的話,那麼碰撞的幾率就會小些,這樣就能提高 HashMap 的性能。

我們可以使用自定義的對象作為鍵嗎? 這是前一個問題的延伸。當然你可能使用任何對象作為鍵,隻要它遵守了

equals()

hashCode()

方法的定義規則,并且當對象插入到Map中之後将不會再改變了。如果這個自定義對象時不可變的,那麼它已經滿足了作為鍵的條件,因為當它建立之後就已經不能改變了。

我們可以使用 CocurrentHashMap 來代替 Hashtable 嗎?這是另外一個很熱門的面試題,因為 ConcurrentHashMap 越來越多人用了。我們知道 Hashtable 是

synchronized

的 ,但是ConcurrentHashMap 同步性能更好,因為它僅僅根據同步級别對map的一部分進行上鎖。ConcurrentHashMap當然可以代替HashTable,但是HashTable提供更強的線程安全性。看看 這篇部落格

檢視Hashtable和ConcurrentHashMap的差別。

我個人很喜歡這個問題,因為這個問題的深度和廣度,也不直接的涉及到不同的概念。讓我們再來看看這些問題設計哪些知識點:

  • hashing的概念
  • HashMap中解決碰撞的方法
  • equals()和hashCode()的應用,以及它們在HashMap中的重要性
  • 不可變對象的好處
  • HashMap多線程的條件競争
  • 重新調整HashMap的大小

    總結

HashMap的工作原理

HashMap基于hashing原理,我們通過put()和get()方法儲存和擷取對象。當我們将鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,讓後找到bucket位置來儲存值對象。當擷取對象時,通過鍵對象的equals()方法找到正确的鍵值對,然後傳回值對象。HashMap使用連結清單來解決碰撞問題,當發生碰撞了,對象将會儲存在連結清單的下一個節點中。 HashMap在每個連結清單節點中儲存鍵值對對象。

當兩個不同的鍵對象的hashcode相同時會發生什麼? 它們會儲存在同一個bucket位置的連結清單中。鍵對象的equals()方法用來找到鍵值對。

因為HashMap的好處非常多,我曾經在電子商務的應用中使用HashMap作為緩存。因為金融領域非常多的運用Java,也出于性能的考慮,我們會經常用到HashMap和ConcurrentHashMap。你可以檢視更多的關于HashMap的文章: