HashMap工作原理

本文來自:http://www.open-open.com/news/view/a369a3

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 使用 LinkedList 存儲對象,這個 Entry (包含有鍵值對的 Map.Entry 對象)會存儲在 LinkedList 中。”這個答案非常的合理,雖然有很多種處理碰撞的方法,這種方法是最簡單的,也正是 HashMap 的處理方法。但故事還沒有完結,面試官會繼續問:

  “如果兩個鍵的 hashcode 相同,你如何獲取值對象?” 面試者會回答:當我們調用 get ()方法,HashMap 會使用鍵對象的 hashcode 找到 bucket 位置,然後獲取值對象。面試官提醒他如果有兩個值對象儲存在同一個 bucket,他給出答案:將會遍歷 LinkedList 直到找到值對象。面試官會問因爲你並沒有值對象去比較,你是如何確定確定找到值對象的?除非面試者直到 HashMap 在 LinkedList 中存儲的是鍵值對,否則他們不可能回答出這一題。

  其中一些記得這個重要知識點的面試者會說,找到 bucket 位置之後,會調用 keys.equals ()方法去找到 LinkedList 中正確的節點,最終找到要找的值對象。完美的答案!

  許多情況下,面試者會在這個環節中出錯,因爲他們混淆了 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 需要重新調整大小了,它們會同時試着調整大小。在調整大小的過程中,存儲在 LinkedList 中的元素的次序會反過來,因爲移動到新的 bucket 位置的時候,HashMap 並不會將元素放在 LinkedList 的尾部,而是放在頭部,這是爲了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那麼就死循環了。這個時候,你可以質問面試官,爲什麼這麼奇怪,要在多線程的環境下使用 HashMap 呢?:)

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

  1. 爲什麼 String, Interger 這樣的 wrapper 類適合作爲鍵? String, Interger 這樣的 wrapper 類作爲 HashMap 的鍵是再適合不過了,而且 String 最爲常用。因爲 String 是不可變的,也是 final 的,而且已經重寫了 equals ()和 hashCode ()方法了。其他的 wrapper 類也有這個特點。不可變性是必要的,因爲爲了要計算 hashCode (),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的 hashcode 的話,那麼就不能從 HashMap 中找到你想要的對象。不可變性還有其他的優點如線程安全。如果你可以僅僅通過將某個 field 聲明成 final 就能保證 hashCode 是不變的,那麼請這麼做吧。因爲獲取對象的時候要用到 equals ()和 hashCode ()方法,那麼鍵對象正確的重寫這兩個方法是非常重要的。如果兩個不相等的對象返回不同的 hashcode 的話,那麼碰撞的機率就會小些,這樣就能提高 HashMap 的性能。
  2. 我們可以使用自定義的對象作爲鍵嗎? 這是前一個問題的延伸。當然你可能使用任何對象作爲鍵,只要它遵守了 equals ()和 hashCode ()方法的定義規則,並且當對象插入到 Map 中之後將不會再改變了。如果這個自定義對象時不可變的,那麼它已經滿足了作爲鍵的條件,因爲當它創建之後就已經不能改變了。
  3. 我們可以使用 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 使用 LinkedList 來解決碰撞問題,當發生碰撞了,對象將會儲存在 LinkedList 的下一個節點中。 HashMap 在每個 LinkedList 節點中儲存鍵值對對象。

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

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章