當我們面試Java開發崗位時,面試官問的頻率出現最多的問題,就是這個HashMap,不管是傳統型公司還是互聯公司,HashMap是必問的,所以作者爆肝整理了HashMap的23個問題以及答案,請查收!
1、你知道HashMap的數據結構嗎?

HashMap底層是基於數組 + 鏈表實現的,不過在 jdk1.7 和 1.8 中具體實現稍有不同
HashMap采用Entry數組來存儲key-value對,每一個鍵值對組成了一個Entry實體,Entry類實際上是一個單向的鏈表結構,它具有Next指針,可以連接下一個Entry實體。只是在JDK1.8中,鏈表長度大於8的時候,鏈表會轉成紅黑樹!
2、什么是Hash沖突,如何解決Hash沖突?
哈希函數的設計至關重要,好的哈希函數會盡可能地保證計算簡單和散列地址分布均勻,但是我們需要清楚的是數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證得到的存儲地址絕對不發生沖突。那么哈希沖突如何解決呢?哈希沖突的解決方案有多種:開放定址法(發生沖突,繼續尋找下一塊未被占用的存儲地址),再散列函數法,鏈地址法(數組+鏈表的形式)。
3、用LinkedList代替數組結構可以嗎?有什么優缺點?
因為用數組效率最高!在HashMap中,定位桶的位置是利用元素的key的哈希值對數組長度取模得到。此時,我們已得到桶的位置。顯然數組的查找效率比LinkedList大;
4、HashMap何時擴容以及它的擴容機制?
如果bucket滿了(超過load factor*current capacity),就要resize。load factor為0.75,為了最大程度避免哈希沖突current capacity為當前數組大小
5、為何HashMap的數組長度一定是2的次冪?
因為2的n次方實際就是1后面n個0,2的n次方-1,實際就是n個1。
例如長度為8時候,3&(8-1)=3 2&(8-1)=2,不同位置上,不碰撞。
而長度為5的時候,3&(5-1)=0 2&(5-1)=0,都在0上,出現碰撞了。
所以,保證容積是2的n次方,是為了保證在做(length-1)的時候,每一位都能&1,保證地址散列均勻分布
6、說一下HashMap在1.7中put元素的過程?

- 判斷當前數組是否需要初始化。
- 如果 key 為空,則 put 一個空值進去。
- 根據 key 計算出 hashcode。
- 根據計算出的 hashcode 定位出所在桶。
- 如果桶是一個鏈表則需要遍歷判斷里面的 hashcode、key 是否和傳入 key 相等,如果相等則進行覆蓋,並返回原來的值。
- 如果桶是空的,說明當前位置沒有數據存入;新增一個 Entry 對象寫入當前位置

- 當調用 addEntry 寫入 Entry 時需要判斷是否需要擴容。
- 如果需要就進行兩倍擴充,並將當前的 key 重新 hash 並定位。
- 而在 createEntry 中會將當前位置的桶傳入到新建的桶中,如果當前桶有值就會在位置形成鏈表。
7、說一下HashMap中get元素的過程?

- 首先也是根據 key 計算出 hashcode,然后定位到具體的桶中。
- 判斷該位置是否為鏈表。
- 不是鏈表就根據 key、key 的 hashcode 是否相等來返回值。
- 為鏈表則需要遍歷直到 key 及 hashcode 相等時候就返回值。
- 啥都沒取到就直接返回 null
8、說一下HashMap在1.8中put元素的過程?
過程跟1.7差不多,但是多了一些判斷:
- 當前鏈表的大小是否大於預設的閾值,大於時就要轉換為紅黑樹;
- 如果當前桶已經為紅黑樹,那就要按照紅黑樹的方式寫入數據;
9、說一下HashMap在1.8中get元素的過程?

-
首先將 key hash 之后取得所定位的桶。
-
如果桶為空則直接返回 null 。
-
否則判斷桶的第一個位置(有可能是鏈表、紅黑樹)的 key 是否為查詢的 key,是就直接返回 value。
-
如果第一個不匹配,則判斷它的下一個是紅黑樹還是鏈表。
-
紅黑樹就按照樹的查找方式返回值。
-
不然就按照鏈表的方式遍歷匹配返回值。
10、HashMap在JDK8做了哪些優化?
- 由數組+鏈表的結構改為數組+鏈表+紅黑樹。
- 優化了高位運算的hash算法:h^(h>>>16)
- 擴容后,元素要么是在原位置,要么是在原位置再移動2次冪的位置,且鏈表順序不變。
11、為什么在解決Hash沖突的時候,不直接用紅黑樹?而是選擇優先使用鏈表,再轉紅黑樹?
- 因為紅黑樹需要進行左旋,右旋,變色這些操作來保持平衡,而單鏈表不需要;
- 當元素小於8個當時候,此時做查詢操作,鏈表結構已經能保證查詢性能;
- 當元素大於8個的時候,此時需要紅黑樹來加快查詢速度,但是新增節點的效率變慢了;
- 如果一開始就用紅黑樹結構,元素太少,新增效率又比較慢,無疑這是浪費性能的;
12、不用紅黑樹用二叉樹可以嗎?
可以。但是二叉查找樹在特殊情況下會變成一條線性結構(這就跟原來使用鏈表結構一樣了,造成很深的問題),遍歷查找會非常慢。
13、當鏈表轉換成紅黑樹后,什么時候退化為鏈表?
- 擴容 resize()時,紅黑樹拆分成的樹的結點數小於等於臨界值6個,則退化成鏈表。
- 移除元素 remove()時,在removeTreeNode()方法會檢查紅黑樹是否滿足退化條件,與結點數無關。如果紅黑樹根root為空,或者root的左子樹/右子樹為空,root.left.left根的左子樹的左子樹為空,都會發生紅黑樹退化成鏈表。
14、HashMap在並發條件下會有什么問題?
- 多線程擴容,引起的死循環問題
- 多線程put的時候可能導致元素丟失
- put非null元素后get出來的卻是null
15、在JDK8中還有這些問題嗎?
在jdk1.8中,死循環問題已經解決。其他兩個問題還是存在
16、HashMap鍵可以是Null嗎?
必須可以,key為null的時候,hash算法最后的值以0來計算,也就是放在數組的第一個位置。
17、你一般使用什么作為HashMap的key?
一般用Integer、String這種不可變類當HashMap當key,而且String最為常用。
- 因為字符串是不可變的,所以在它創建的時候hashcode就被緩存了,不需要重新計算。這就使得字符串很適合作為Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵往往都使用字符串。
- 因為獲取對象的時候要用到equals()和hashCode()方法,那么鍵對象正確的重寫這兩個方法是非常重要的,這些類已經很規范的覆寫了hashCode()以及equals()方法。
18、當使用對象作為HashMap的Key時有什么問題嗎?
要重寫equals和hashcode方法。否則會出現以下問題:
hashcode可能發生改變,導致put進去的值,無法get出,如下所示

輸出值如下:
19、HashMap是線程安全的嗎?如何實現線程安全?
HashMap是線程不安全的。
實現方式:
- 通過Collections.synchronizedMap()來封裝所有不安全的HashMap的方法,就連toString, hashCode都進行了封裝,就是為每一個方法添加了synchronized關鍵字進行修飾。使用的是的synchronized方法,是一種悲觀鎖.在進入之前需要獲得鎖,確保獨享當前對象,然后做相應的修改/讀取。方式簡單粗暴,但是效率低。
- 使用ConcurrentHashMap。只有在需要修改對象時,比較和之前的值是否被人修改了,如果被其他線程修改了,那么就會返回失敗,是一種無鎖的實現。基於CAS實現,類似於樂觀鎖機制。ConcurrentHashMap采用了"鎖分段"策略,ConcurrentHashMap的主干是一個一個Segment組,在ConcurrentHashMap中,一個Segment就是一個子哈希表,Segment里維護了一個HashEntry數組,並發環境下,對於不同Segment的數據進行操作是不用考慮鎖競爭的,對於同一個Segment的操作才需考慮線程同步。理論上就允許16個線程並發執行。
20、請談談ConcurrentHashMap底層實現原理?
在JDK7中ConcurrentHashMap采用了"鎖分段"策略,ConcurrentHashMap的主干是一個一個Segment組,在ConcurrentHashMap中,一個Segment就是一個子哈希表,Segment里維護了一個HashEntry數組,並發環境下,對於不同Segment的數據進行操作是不用考慮鎖競爭的,對於同一個Segment的操作才需考慮線程同步。理論上就允許16個線程並發執行。
21、ConcurrentHashMap的size()方法實現原理?
- 要統計整個ConcurrentHashMap的元素個數,可以將每個Segment的count相加,count是volatile變量,可以保證讀到的是最新值,但count可能會在累加過程中發生改變,導致結果不正確。
- ConcurrentHashMap采用HashMap中的“快速失敗”機制,即設置一個modCount變量,在put,remove,clean方法中都讓modCount++,先嘗試兩次通過不對Segment加鎖的方式統計Size,若發現前后的modCount不一致,則說明容器大小發生了變化,此時再通過鎖住所有Segment的put,remove,clean方法計算count。
22、ConcurrentHashMap中put過程?
因為volatile不保證原子性,所以在put操作中需要對Segment加鎖。
put操作分為兩步:
- 是否需要擴容
- 在插入元素前先判斷Segment里的HashEntry數組是否超過容量(cap*loadFactor),如果超過閾值,就進行擴容。值得一提的是,在HashMap中,是先插入元素后再檢查是否達到容量,有可能造成擴容之后再也沒有新元素插入,造成空間浪費。
- 舉個例子,在ConcurrentHashMap中,現有元素正好等於容量,那么就先判斷是否超過容量(沒有超過),那么添加新元素(此時超出容量一個元素,但沒有擴容)。而如果是HashMap,則先插入這個元素,發現超出容量,於是擴容,可再也沒有新的元素添加進來了,於是造成了浪費。
- 定位元素位置
- 遍歷HashEntry鏈表,找到對應元素位置並更新
23、HashMap和HashTable的區別?
- HashMap基於數組和鏈表實現。不考慮Hash沖突的情況下,僅需一次定位就能找到元素。比如在新增元素的時候,通過Hash函數將元素定位Hash表中某個位置,直接將數據存入到該地址上,當我們查找或者刪除元素,可以直接通過Hash函數定位到該數據。但是沒有什么事情都是完美的,如果兩個不同的元素,通過哈希函數得出的實際存儲地址相同怎么辦?也就是說,當我們對某個元素進行哈希運算,得到一個存儲地址,然后要進行插入的時候,發現已經被其他元素占用了,其實這就是所謂的哈希沖突,也叫哈希碰撞。HashMap采用了鏈地址法,也就是數組+鏈表的方式。把相同Hash值的數據放在了鏈表上。當HashMap中的鏈表出現越少,性能才會越好。當發生哈希沖突並且size大於閾值的時候,需要進行數組擴容,擴容時,需要新建一個長度為之前數組2倍的新的數組,然后將當前的Entry數組中的元素全部傳輸過去,擴容后的新數組長度為之前的2倍,所以擴容相對來說是個耗資源的操作。HashMap繼承自AbstractMap,HashMap允許key、value為空。HashMap默認容量是16,且負載因子是0.75。HashMap是線程不安全的,效率高。
- HashTable和HashMap的實現原理幾乎一樣,HashTable不允許key和value為null;HashTable是線程安全的。但是HashTable線程安全的策略實現代價卻太大了,簡單粗暴,get/put所有相關操作都是synchronized的,這相當於給整個哈希表加了一把大鎖,多線程訪問時候,只要有一個線程訪問或操作該對象,那其他線程只能阻塞,相當於將所有的操作串行化,在競爭激烈的並發場景中性能就會非常差。
以上是整理的比較全面的HashMap面試題,大家記住答案的同時,最好還是理解其原理!!!