當我們面試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面試題,大家記住答案的同時,最好還是理解其原理,往期精彩面試題解析回顧:
- JAVA面試題 String s = new String("xyz");產生了幾個物件?
- Java面試題 從原始碼角度分析HashSet實現原理?
- JAVA面試題 請談談你對Sychronized關鍵字的理解?
- JAVA面試題 執行緒的生命週期包括哪幾個階段? - Java螞蟻 - 部落格園 (cnblogs.com)
- JAVA面試題 StringBuffer和StringBuilder的區別,從原始碼角度分析?
- JAVA面試題 手寫ArrayList的實現,在筆試中過關斬將?
- JAVA面試題 淺析Java中的static關鍵字?
- JAVA面試題 啟動執行緒是start()還是run()?為什麼?
- Java面試題 equals()與"=="的區別?