HashMap工作原理

前言

幾乎每個Java程序員都知道HashMap,都知道哪里要用HashMap,知道Hashtable和HashMap之間的區(qū)別
,那么為何這個問題如此特殊呢?是因為這個問題的深度很深。ConcurrentHashMap和其它同步集合的引入讓這道題變得更加復雜。讓我們開始探索的旅程吧!


循序漸進

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

幾乎每個人都會回答“是的”,然后回答

  • HashMap的一些特性,譬如HashMap可以接受null鍵值和值,而Hashtable則不能;
  • HashMap是非synchronized;而HashTable是synchronized的
  • HashMap對于HashTable來說很快;
  • 以及HashMap儲存的是鍵值對等等。

這顯示出你已經用過HashMap,而且對它相當的熟悉。關于HashMap的更多基礎的細節(jié)??赡軙瓿鱿旅娴膯栴}:

“你知道HashMap的工作原理嗎?” “你知道HashMap的get()方法的工作原理嗎?”

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

但一些人可能可以給出答案:
“HashMap是基于hashing的原理,我們使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。當我們給put()方法傳遞鍵和值時,我們先對鍵調用hashCode()方法,返回的hashCode用于找到bucket位置來儲存Entry對象。”
這里關鍵點在于指出,HashMap是在bucket中儲存鍵對象和值對象,作為Map.Entry。這一點有助于理解獲取對象的邏輯。如果你沒有意識到這一點,或者錯誤的認為僅僅只在bucket中存儲值的話,你將不會回答如何從HashMap中獲取對象的邏輯。這個答案相當的正確,也顯示出面試者確實知道hashing以及HashMap的工作原理。但是這僅僅是故事的開始,當繼續(xù)加入一些Java程序員每天要碰到的實際場景的時候,錯誤的答案頻現。下個問題可能是關于HashMap中的碰撞探測(collision detection)以及碰撞的解決方法:

“當兩個對象的hashcode相同會發(fā)生什么?”

從這里開始,真正的困惑開始了,一些人會回答因為hashcode相同,所以兩個對象是相等的,HashMap將會拋出異常,或者不會存儲它們。然后別人可能會提醒他們有equals()和hashCode()兩個方法,并告訴他們兩個對象就算hashcode相同,但是它們可能并不相等。一些人可能就此放棄,而另外一些還能繼續(xù)挺進,他們回答“因為hashcode相同,所以它們的bucket位置相同,‘碰撞’會發(fā)生。因為HashMap使用鏈表存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在鏈表中。”這個答案非常的合理,雖然有很多種處理碰撞的方法,這種方法是最簡單的,也正是HashMap的處理方法。但故事還沒有完結,我們還可以繼續(xù)問:

“如果兩個鍵的hashcode相同,你如何獲取值對象?”

一些人會回答:當我們調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,然后獲取值對象。但別人提醒他如果有兩個值對象儲存在同一個bucket,他給出答案:將會遍歷鏈表直到找到值對象。那我會問因為你并沒有值對象去比較,你是如何確定確定找到值對象的?除非找到HashMap在鏈表中存儲的是鍵值對,否則不可能回答出這一題。

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

許多情況下,會在這個環(huán)節(jié)中出錯,因為他們混淆了hashCode()和equals()方法。因為在此之前hashCode()屢屢出現,而equals()方法僅僅在獲取值對象的時候才出現。一些優(yōu)秀的開發(fā)者會指出使用不可變的、聲明作final的對象,并且采用合適的equals()和hashCode()方法的話,將會減少碰撞的發(fā)生,提高效率。不可變性使得能夠緩存不同鍵的hashcode,這將提高整個獲取對象的速度,使用String,Interger這樣的wrapper類作為鍵是非常好的選擇。那么我繼續(xù)問:

“為什么String, Interger這樣的wrapper類適合作為鍵?”

String, Interger這樣的wrapper類作為HashMap的鍵是再適合不過了,而且String最為常用。因為String是不可變的,也是final的,而且已經重寫了equals()和hashCode()方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashcode的話,那么就不能從HashMap中找到你想要的對象。不可變性還有其他的優(yōu)點如線程安全。如果你可以僅僅通過將某個field聲明成final就能保證hashCode是不變的,那么請這么做吧。因為獲取對象的時候要用到equals()和hashCode()方法,那么鍵對象正確的重寫這兩個方法是非常重要的。如果兩個不相等的對象返回不同的hashcode的話,那么碰撞的幾率就會小些,這樣就能提高HashMap的性能。

如果你認為到這里已經完結了,那么聽到下面這個問題的時候,你會大吃一驚。

“如果HashMap的大小超過了負載因子(load factor)定義的容量,怎么辦?”

除非你真正知道HashMap的工作原理,否則你將回答不出這道題。默認的負載因子大小為0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會創(chuàng)建原來HashMap大小的兩倍的bucket數組,來重新調整map的大小,并將原來的對象放入新的bucket數組中。這個過程叫作rehashing,因為它調用hash方法找到新的bucket位置。

如果你能夠回答這道問題,下面的問題來了:

“你了解重新調整HashMap大小存在什么問題嗎?”

你可能回答不上來,這時我會提醒你當多線程的情況下,可能產生條件競爭(race condition)。

當重新調整HashMap大小的時候,確實存在條件競爭,因為如果兩個線程都發(fā)現HashMap需要重新調整大小了,它們會同時試著調整大小。在調整大小的過程中,存儲在鏈表中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap并不會將元素放在鏈表的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭發(fā)生了,那么就死循環(huán)了。這個時候,你可以質問一些提問者,為什么這么奇怪,要在多線程的環(huán)境下使用HashMap呢?:)

然后再來一波,其實

我們可以使用CocurrentHashMap來代替Hashtable嗎?

這是另外一個很熱門的面試題,因為ConcurrentHashMap越來越多人用了。我們知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因為它僅僅根據同步級別對map的一部分進行上鎖。ConcurrentHashMap當然可以代替HashTable,HashMap現在已經是過時方法了,所以現在更建議使用ConcurrentHashMap。


總結
其實這樣一系列問下來可以涉及到的知識點是非常的多:

  • hashing的概念
  • HashMap中解決碰撞的方法
  • equals()和hashCode()的應用,以及它們在HashMap中的重要性
  • 不可變對象的好處
  • HashMap多線程的條件競爭
  • 重新調整HashMap的大小
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容