背過不怕面試問 HashMap!

Java 8 之前:

底層實現是數組 + 鏈表,主要成員變量包括:存儲數據的 table 數組、鍵值對數量 size、加載因子 loadFactor。

table 數組用於記錄 HashMap 的所有數據,它的每一個下標都對應一條鏈表,所有哈希衝突的數據都會被存放到同一條鏈表中,Entry 是鏈表的節點元素,包含四個成員變量:鍵 key、值 value、執行下一個節點的指針 next 和 元素的散列值 hash。

在 HashMap 中數據都是以鍵值對的形式存在的,鍵對應的 hash 值將會作爲其在數組裏的下標,如果兩個元素 key 的 hash 值一樣,就會發送哈希衝突,被放到同一個下標中的鏈表上,爲了使 HashMap 的查詢效率儘可能高,應該使鍵的 hash 值儘可能分散。

HashMap 默認初始化容量爲 16,擴容容量必須是 2 的冪次方、最大容量爲 1<< 30 、默認加載因子爲 0.75。

1 put 方法:添加元素

① 如果 key 爲 null 值,直接存入 table[0]。

② 如果 key 不爲 null 值,先計算 key 對應的散列值。

③ 調用 indexFor 方法根據 key 的散列值和數組的長度計算元素存放的下標 i。

④ 遍歷 table[i] 對應的鏈表,如果 key 已經存在,就更新其 value 值然後返回舊的 value 值。

⑤ 如果 key 不存在,就將 modCount 的值加 1,使用 addEntry 方法增加一個節點,並返回 null 值。

2 hash 方法:計算元素 key 對應的散列值

① 處理 String 類型的數據時,直接調用對應方法來獲取最終的hash值。

② 處理其他類型數據時,提供一個相對於 HashMap 實例唯一不變的隨機值 hashSeed 作爲計算的初始量。

③ 執行異或和無符號右移操作使 hash 值更加離散,減小哈希衝突的概率。

3 indexFor 方法:計算元素下標

直接將 hash 值和數組長度 - 1 進行與操作並返回,保證計算後的結果不會超過 table 數組的長度範圍。

4 resize 方法:根據newCapacity 來確定新的擴容閾值 threshold

① 如果當前容量已經達到了最大容量,就將閾值設置爲 Integer 的最大值,之後擴容就不會再觸發。

② 創建一個新的容量爲 newCapacity 的 Entry 數組,並調用 transfer 方法將舊數組的元素轉移到新數組.

③ 將閾值設爲(newCapacity 和加載因子 loadFactor 的積)和(最大容量 + 1 )的較小值。

transfer:轉移舊數組到新數組

① 遍歷舊數組的所有元素,調用 rehash 方法判斷是否需要哈希重構,如果需要就重新計算元素 key 的散列值。

② 調用 indexFor 方法根據 key 的散列值和數組的長度計算元素存放的下標 i,將舊數組的元素轉移到新的數組。

5 get 方法:根據 key 獲取元素的 value 值

① 如果 key 爲 null 值,調用 getForNullKey 方法,如果 size 爲 0 表示鏈表爲空,返回 null 值。如果 size 不爲 0,說明存在鏈表,遍歷 table[0] 的鏈表,如果找到了 key 爲 null 的節點則返回其 value 值,否則返回 null 值。

② 調用 getEntry 方法,如果 size 爲 0 表示鏈表爲空,返回 null 值。如果 size 不爲 0,首先計算 key 的散列值,然後遍歷該鏈表的所有節點,如果節點的 key 值和 hash 值都和要查找的元素相同則返回其 Entry 節點。

③ 如果找到了對應的 Entry 節點,使用 getValue 方法獲取其 value 值並返回,否則返回 null 值。

Java 8 之後:

使用的是數組 + 鏈表/紅黑樹的形式,table 數組的元素數據類型換成了 Entry 的靜態實現類 Node。

1 put 方法:添加元素

① 調用 putVal 方法添加元素。

② 如果 table 爲空或沒有元素時就進行擴容,否則計算元素下標位置,如果不存在就新創建一個節點存入。

③ 如果首節點和待插入元素的 hash值和 key 值都一樣,直接更新 value 值。

④ 如果首節點是 TreeNode 類型,調用 putTreeVal 方法增加一個樹節點,每一次都比較插入節點和當前節點的大小,待插入節點小就往左子樹查找,否則往右子樹查找,找到空位後執行兩個方法:balanceInsert 方法,一方面把節點插入紅黑樹,一方面對紅黑樹進行調整使之平衡。moveRootToFront 方法,由於紅黑樹調整平衡後 root節點可能變化,table裏記錄的節點不再是根節點,需要重置根節點。

⑤ 如果是鏈表節點,就遍歷鏈表,根據 hash值和 key 值判斷是否重複,決定更新值還是新增節點。如果遍歷到了鏈表末尾,添加鏈表元素,如果達到了建樹閾值,還需要調用 treeifyBin 方法把鏈表重構爲紅黑樹。

⑥ 存放元素後,將 modCount 值加 1,如果節點數 + 1大於擴容閾值,還需要進行擴容。

2 get 方法:根據 key 獲取元素的 value 值

① 調用 getNode 方法獲取 Node 節點,如果不是 null 值就返回 Node 節點的 value 值,否則返回 null。

② 如果數組不爲空,先比較第一個節點和要查找元素的 hash 值和 key 值,如果都相同則直接返回。

③ 如果第二個節點是 TreeNode 節點則調用 getTreeNode 方法進行查找,否則遍歷鏈表根據 hash 值和 key 值進行查找,如果沒有找到就返回 null。

3 hash 方法:計算元素 key 對應的散列值

Java 8 的計算過程簡單了許多,如果 key 非空就將 key 的 hashCode() 返回值的高低16位進行異或操作,這主要是爲了讓儘可能多的位參與運算,讓結果中的 0 和 1 分佈得更加均勻,從而降低哈希衝突的概率。

4 resize 方法:擴容數組

重新規劃長度和閾值,如果長度發生了變化,部分數據節點也要重新排列。

重新規劃長度

① 如果 size 超出擴容閾值,把 table 容量增加爲之前的2倍。

② 如果新的 table 容量小於默認的初始化容量16,那麼將 table 容量重置爲16。

③ 如果新的 table 容量大於等於最大容量,那麼將閾值設爲 Integer 的最大值,並且 return 終止擴容,由於 size 不可能超過該值因此之後不會再發生擴容。

重新排列數據節點

① 如果節點爲 null 值則不進行處理。

② 如果節點不爲 null 值且沒有next節點,那麼重新計算其散列值然後存入新的 table 數組中。

③ 如果節點爲 TreeNode 節點,那麼調用 split 方法進行處理,該方法用於對紅黑樹調整,如果太小會退化回鏈表。

④ 如果節點是鏈表節點,需要將鏈表拆分爲 hashCode() 返回值超出舊容量的鏈表和未超出容量的鏈表。對於hash & oldCap == 0的部分不需要做處理,反之需要放到新的下標位置上,新下標 = 舊下標 + 舊容量。


補充:紅黑樹

紅黑樹是一種自平衡的二叉查找樹。

特性: 紅黑樹的每個節點只能是紅色或者黑色、根節點是黑色的、每個葉子節點都是黑色的、如果一個葉子節點是紅色的,它的子結點必須是黑色的、從一個節點到該節點的葉子節點的所有路徑都包含相同數目的黑色節點。

左旋: 對 a 節點進行左旋,指將 a 節點的右節點作爲 a 的父節點,即將a變成一個左節點。

右旋: 對 a 節點進行右旋,指將 a 節點的左節點作爲 a 的父節點,即將a變成一個右節點。

添加

紅黑樹的添加分爲3步:① 將紅黑樹看作一顆二叉查找樹,並以二叉樹的插入規則插入新節點。② 將插入的節點設爲紅色或黑色。③ 通過左旋、右旋或變色,使之重新成爲一棵紅黑樹。

根據被插入節點的父節點情況,可以將插入分爲3種情況處理:

  • 被插入的節點是根節點,直接將其塗爲黑色。
  • 被插入節點的父節點是黑色的,不做處理,節點插入後仍是紅黑樹。
  • 被插入節點的父節點是紅色的,一定存在非空祖父節點,進一步分爲三種情況處理:
    • 叔叔節點是紅色的,將父節點設爲黑色,叔叔節點設爲黑色,祖父節點設爲紅色,將祖父節點作爲當前節點。
    • 叔叔節點是黑色的且當前節點是右節點,則將父節點設爲當前節點,以新節點爲支點左旋。
    • 叔叔節點是黑色的且當前節點是左節點,則將父節點設爲黑色,祖父節點設爲紅色,以祖父節點爲支點右旋。

刪除

紅黑樹的添加分爲3步:① 將紅黑樹看作一顆二叉查找樹,並以二叉樹的刪除規則刪除新節點。② 通過左旋、右旋或變色,使之重新成爲一棵紅黑樹。

根據被刪除節點的情況,可以將刪除分爲3種情況處理:

  • 被刪除的節點沒有子節點,直接將其刪除。
  • 被刪除節點只有一個子節點,直接刪除該節點,並用其唯一子節點替換其位置。
  • 被插入節點有兩個子節點,先找出該節點的替換節點,然後把替換節點的數值複製給該節點,刪除替換節點。

通過左旋、右旋或變色使其重新成爲紅黑樹。如果當前節點的子節點是一紅一黑,直接將該節點設爲黑色。如果當前節點的子結點都是黑色,且當前節點是根節點,則不做處理。如果當前節點的子節點都是黑色且當前節點不是根節點,分爲以下幾種情況:

  • 當前節點的兄弟節點是紅色的,就將當前節點的兄弟節點設爲黑色,將父節點設爲紅色,對父節點左旋,重新設置當前節點的兄弟節點。
  • 當前節點的兄弟節點是黑色的,兄弟節點的兩個子節點也都是黑色的,則將當前節點的兄弟節點設爲紅色,將當前節點的父節點作爲新節點。
  • 當前節點的兄弟節點是黑色的,兄弟節點的左節點是紅色右節點是黑色,將當前節點的左子結點設爲黑色,將兄弟節點設爲紅色,對兄弟節點右旋、重新設置兄弟節點。
  • 當前節點的兄弟節點是黑色的,兄弟節點的右節點是紅色的,將當前節點的父節點賦給兄弟節點,將父節點設爲黑色,將兄弟節點的右子節點設爲黑色,對父節點左旋,設置當前節點爲根節點。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章