【Java 容器面試題】談談你對HashMap 的理解

言技發表於2018-12-22

  為了能夠在面試回答中優雅而不失體面回答面試考點,該文章借鑑了不同平臺對知識點的描述。

  • 如有侵權請聯絡我
  • 文章的不足和錯誤請指正,好的建議也不要吝嗇,我都會採納並更正
  • 您的點贊是我持續更新的動力

我的回答

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 面試怎麼答,以及需要注意的知識點,希望對你有所幫助。

HashMap 的東西太多,因個人能力有限不能一一道全,後面如果變強了我會重新補全

推薦一篇關於HashMap 1.8 的比較好的部落格:HashMap 1.8 重大更新

來源:https://juejin.im/post/5c1da988f265da6143130ccc

相關文章