面試官:談談你對 HashMap 的理解

回答

HashMap 是一種存取高效但不保證有序的常用容器。它的數據結構爲“數組+鏈表”,是解決哈希衝突的產物,也就是我們常說的鏈地址法。它實現了Map 接口採用K-V 鍵值對存儲數據,並實現了淺拷貝和序列化。

HashMap 的默認初始大小爲16,初始化大小必須爲2的冪,最大大小爲2的30次方。數組中存儲的鏈表節點Entry 類實現於Map.Entry 接口,它實現了對節點的通用操作。

HashMap 的閾值默認爲“容量*0.75f”,當存儲節點數量超過該值,則對map 進行擴容處理。

HashMap 提供了4種構造方法,分別是默認構造方法;可以指定初始容量的構造方法;可以指定初始容量和閾值的構造方法以及基於一個Map 的構造方法。雖然是構造函數,但是真正的初始化都是在第一次添加操作裏面實現的。

在第一次添加操作中,HashMap 會先判斷存儲數組有沒有初始化,如果沒有先進行初始化操作,初始化過程中會取比用戶指定的容量大的最近的2 的冪次方數作爲數組的初始容量,並更新擴容的閾值。

接着添加操作講解。添加操作的執行流程爲:

  • 先判斷有沒有初始化
  • 再判斷傳入的key 是否爲空,爲空保存在table[o] 位置
  • key 不爲空就對key 進hash,hash 的結果再& 數組的長度就得到存儲的位置
  • 如果存儲位置爲空則創建節點,不爲空就說明存在衝突
  • 解決衝突HashMap 會先遍歷鏈表,如果有相同的value 就更新舊值,否則構建節點添加到鏈表頭
  • 添加還要先判斷存儲的節點數量是否達到閾值,到達閾值要進行擴容
  • 擴容擴2倍,是新建數組所以要先轉移節點,轉移時都重新計算存儲位置,可能保持不變可能爲舊容量+位置。
  • 擴容結束後新插入的元素也得再hash 一遍才能插入。

獲取節點的操作和添加差不多,也是

  • 先判斷是否爲空,爲空就在table[0] 去找值
  • 不爲空也是先hash,&數組長度計算下標位置
  • 再遍歷找相同的key 返回值

HashMap 的其他操作大同小異,再講講HashMap1.7 的問題還有1.7 和1.8 的差別。

HashMap 是一個併發不安全的容器,在迭代操作是採用的是fast-fail 機制;在併發添加操作中會出現丟失更新的問題;因爲採用頭插法在併發擴容時會產生環形鏈表的問題,導致CPU 到達100%,甚至宕機。

解決併發問題可以採用

  • Java 類庫提供的Collections 工具包下的Collections.synchronizedMap()方法,返回一個線程安全的Map
  • 或者使用併發包下的 ConcurrentHashMap,ConcurrentHashMap採用分段鎖機制實現線程安全
  • 使用HashTable (不推薦)

Hash1.7 和1.8 最大的不同在於1.8 採用了“數組+鏈表+紅黑樹”的數據結構,在鏈表長度超過8 時,把鏈表轉化成紅黑樹來解決HashMap 因鏈表變長而查詢變慢的問題;其次

  • 在hash 取下標時將1.7 的9次擾動(5次按位與和4次位運算)改爲2次(一次按位與和一次位運算)
  • 1.7 的底層節點爲Entry,1.8 爲node ,但是本質一樣,都是Map.Entry 的實現
  • 還有就是在存取數據時添加了關於樹結構的遍歷更新與添加操作,並採用了尾插法來避免環形鏈表的產生
  • 但是併發丟失更新的問題依然存在。

回答順序:數據結構+繼承結構+基本字段+構造方法+添加操作+擴容操作+獲取操作+併發問題+與1.8的區別

考點分析

HashMap 作爲最基本的容器,它本身的設計與1.7 1.8的差異性導致HashMap 成爲面試中最最高頻的考點。所以掌握HashMap 勢在必行,但是想要在各種寬泛的回答中脫穎而出,就必須對hashMap 前因後果瞭然於胸。

考點一:爲什麼初始容量必須爲2 的冪?爲什麼負載因子爲0.75f?爲什麼要做那麼多擾動處理?

這些問題都要圍繞一個點來回答:減少哈希衝突。

(1)容量必須爲2 的冪是爲了增加取值的可能性。

2 的n次冪轉化爲二進制爲1後面n個0,在計算下標的時候是hash&(length - 1),也就是&(n-1)個1:初始容量爲4->100,length-1 -> 11。所有的二進制爲都爲1有什麼好處?

  • 0/1 & 1 都爲它本身
  • 0/1 & 0 都爲 0

可以看出&1保證了取值的平均。如果某一位爲0 ,比如最後一位,那麼它&出來下標就一定是個偶數,減少了HashMap 數組一半的取值,大大增加了衝突的可能。

(2)負載因子爲0.75f 是空間與時間的均衡

如果負載因子小,意味着閾值變小。比如容量爲10 的HashMap,負載因子爲0.5f,那麼存儲5個就會擴容到20,出現哈希衝突的可能性變小,但是空間利用率不高。適用於有足夠內存並要求查詢效率的場景。

相反如果閾值爲1 ,那麼容量爲10,就必須存儲10個元素才進行擴容,出現衝突的概率變大,極端情況下可能會從O(1)退化到O(n)。適用於內存敏感但不要求要求查詢效率的場景

(3)hash() 的意義在於使hash 結果不同

hash 算法的好壞直接影響hash 結構的效率,壞的hash 算法極端情況下可能會使hash 結構的存取效率從O(1)退化到O(n)。1.8 之所以把9 次擾動降到2 次,是出於計算效率的考慮。

考點二:& 字符雖然和 % 效果一樣,但是操作效率更高

考點三:爲什麼int,String 適合最爲key?

int 和 String 的好處在於hash 出來的值不會改變。如果是一個對象,那麼他們可能會因爲內部引用的改變而hashCode 值的改變,會導致存儲重複的數據或找不到數據的情況。

考點四:併發操作導致的添加丟失和環形鏈表的產生過程

知識點拓展

不僅僅是HashMap 的東西,根據你的回答,面試官會引出很多其他的問題,所以你在自己設計回答的過程中可以有意識引導面試官問出你熟悉的內容,安排的明明白白。

拓展一:解決Hash 衝突的不同方案

  • 鏈地址法
  • 開發地址:線性探測法、平方探測法
  • 完全散列:布穀鳥散列

拓展二:HashMap 是淺拷貝,說一說淺拷貝和深拷貝的區別

拓展三:說一說Collections.synchronizedMap()和HashTable 的區別

拓展四:說一說HashMap 如何實現有序(LinkHashMap 和TreeMap)以及他們的差別

拓展五:說一說ConcurrentHashMap 如何實現線程安全

結尾

這篇文章更多的是HashMap 面試怎麼答,以及需要注意的知識點,希望對你有所幫助。

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