HashMap put原理詳解(基於jdk1.8)
此文轉載自:https://blog.csdn.net/weixin_49631226/article/details/110247453
前言
本文是個人對Hashmap的一些個人見解,主要通過使用hashmapput的一些程式碼來闡述其底層實現原理,在面試中也會經常會用到,如有不對的地方望大家指正。
(1)先描述一下hashmap的一個底層資料結構:
Hashmap底層是由陣列和連結串列結合實現的。如下圖:
其中每個table[x](或table[x].next...)都是一個node,都是一個插入的元素
Node實體類為:
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; ... }
可以看到有四個引數:hash(hash值)、key(我們平常put的key)、value(put的value)、next(hashMap資料結構圖中的.next,也就是記錄連結串列中每個元素的後繼元素)
1.建立hashmap物件:Map<String,Object> map = new HashMap<>();
建立一個map物件,物件中的一些引數屬性被賦予預設值,主要有參考容量(capacity)、擴容閾值(threshold)和負載因子(loadFactor),下面簡單介紹一下這些引數屬性的作用和關係。
(1)參考容量(capacity)
用來作為建立map物件中Node[]陣列的初始長度(容量)的參考,
可以自己指定長度,指定方式為:Map<String,Object> map = new HashMap<>(capacity);//capacity的值就是你要指定的長度
這裡之所以說是參考,是因為hashmap內部有一個機制:建立map物件中Node[]陣列的初始長度必須要是2的n次方(原因在2.(3)),當你設定長度是23的時候,hashmap會把初始長度設定成32。因為23在16(2的4次方)到32(2的5次方)之間,取最大的數32。
(2)擴容閾值(threshold)和負載因子(loadFactor)
hashmap在新增元素的過程中,如果達到擴容閾值,就會擴大Node[]陣列的長度
其預設值為:參考容量 *負載因子
而負載因子的預設值為0.75,可以修改但是不建議修改。
2.首次put:map.put("haha","haha的值");
將"haha的值"插入到"haha"的鍵的node中。
這裡可以帶幾個問題:
元素要想把值插到陣列+連結串列的結構中的話,首先要接觸陣列,然後再接觸連結串列
那我們怎麼知道要把元素放在陣列的哪個位置(陣列下標怎麼得到)?
什麼情況下會把元素放在連結串列裡?
(1)計算出"haha"的hash值
int hash = hash("haha");
(2)判斷陣列是否為空
判斷Node[]陣列是否為空,為空的話按照初始長度開闢陣列空間
(3)對hash值作減一與運算得到陣列下標
直接算出來的hash值可能非常大,不可能直接當作陣列下標的。對此hashmap的設計者有自己的解決方案:求餘
也就是:index = hash值 %陣列長度
這樣的話index的值永遠都在陣列長度之內,也就可以作為陣列下標了
但是這樣做有一個缺點:效率低,於是hashmap的設計者把求餘改成了一個效率更高的運算:減一與運算
也就是:index =hash值 & (陣列長度-1)
為什麼這樣得出來的index也在陣列長度之內呢?可以看下例子(由於是位運算,需要把hash值和陣列長度分解成二進位制,這樣看的更清楚,假設它兩二進位制只有八位):
陣列長度: 0001 0000
陣列長度-1: 0000 1111
hash值: 1101 0101
與操作: 0000 0101
可以看到,陣列長度-1後,前四位變成了0,跟hash值作與操作之後,hash值前四位不管是什麼,都會變成0,而後四位由於對方都是1,因此得以保留下來。這樣得到最後的結果永遠都不會超過陣列長度。
這裡必須要滿足一個前提條件:陣列長度必須要是2的n次方。因為這樣才能保證陣列長度-1之後,前面為0,後面為1。
(4)把值賦給對應的node
陣列下標拿到了,要插入的位置也就基本確定了。在插入之前,hashmap會去判斷當前陣列位置上有沒有元素,由於我們這是第一次插入,因此這裡就是直接插入元素。
這裡插入的方式很簡單,就是把node的四大引數(hash值、key、value、next)賦給當前陣列位置上的node。由於是位置上第一個元素,後繼沒有連結串列元素,next的值就是null。
(5)插入後操作
插入之後,hashmap的全域性變數:size,也就是已有元素數量,加一,然後看下有沒有大於擴容閾值,如果大的話就要擴容。
(6)最終效果(這裡設"haha"算出的index為2)
3.put不同key不同index的值:map.put("heihei","heihei的值");
這裡的過程和2.一樣,最終結果為(這裡設"heihei"算出的index為0):
4.put相同key的值:map.put("haha","haha另一個值");
這裡得到陣列下標值之後,發現位置上已經有元素了,這種情況就叫做雜湊碰撞
這時候,就要看看這個位置上的元素是不是同一個key,是的話就覆蓋,不是的話就追加到後繼連結串列——這就是連結串列的作用
怎麼判斷是不是同一個key呢?
(1)hash值是否相等
首先要看看計算出來的hash值是否相等,這裡算出來的是相等的。
(2)兩個key是否相等
hash值相等,兩個key不一定相等,因為算hash值需要呼叫hashCode()方法,這個方法我們是可以自己複寫的,可能會出現很多不同結果相等的情況。
所以我們需要通過"=="和equals()來判斷兩個key是否相等。這裡算出來的是相等的。
為什麼不直接判斷是否相等,而還要先判斷hash值?
因為hash值不相等,那肯定不是同一個key。算hash值相對來說速度快點,有這個先把關會大大提高效率,而不是上來就==和equals。
(3)覆蓋原值
判斷是同一個key,那就直接覆蓋原值就行了,最終結果為:
5.首次put不同key相同index的值:map.put("haha2","haha2的值")
這裡假設"haha2"算出來的index和"haha"一樣。
在這個前提下,自然就會出現雜湊碰撞。這時候的過程也是和4.一樣,區別在於在比對key的時候,發現兩個key的hash值不相等,也就不是同一個key
之後自然會把這個元素插到"haha"元素後面形成連結串列
插入的方式很簡單,就是把"haha2"元素賦給"haha"的next,"haha2"的next為null
最終結果為:
這裡插入之前會進行一些判斷,比如是否是紅黑樹等等,後面的情況會細講。
6.繼續put不同key相同index的值:map.put("haha3","haha3的值")
這裡假設"haha3"算出來的index和"haha"一樣。
過程和5.一樣,區別在於在比對的時候,會遍歷當前陣列位置上鍊表的所有元素,如果發現相同key則覆蓋,沒有相同key就追加到連結串列後面。
最終結果為:
7.put到連結串列轉成紅黑樹:map.put("haha8","haha8的值")
這裡假設"haha8"算出來的index和"haha"一樣。
這裡假設連結串列上的元素已經有7個了。
前面我們發現,隨著put的元素越來越多,雜湊碰撞的次數也越來越頻繁,導致連結串列會越來越長,這樣每次put要遍歷的元素也越來越多,會讓hashmap的效能越來越差。
(1)連結串列轉成紅黑樹
於是紅黑樹的解決方案就出現了。紅黑樹是一種能讓遍歷的效率更高的一種資料結構。
當連結串列的元素數量達到8的時候,當前連結串列自動轉成紅黑樹的形式儲存,能大大提升遍歷效率。
(2)擴容機制
除了紅黑樹,還有沒有什麼解決方案可以提高效能呢?
有——擴容機制
之所以連結串列越來越長,是因為雜湊碰撞的次數太頻繁了。那我們可以通過降低這種次數來提升效能。擴容可以做到這一點。
擴容是增加陣列的長度,這樣我們就有更多的空間去分配連結串列的位置,雜湊碰撞的次數就會少很多。
類似於我們去餐廳吃飯,位置越多我們吃上飯的概率就越大,而不用老是等位(遍歷)。
hashmap的擴容過程簡單來說就是新建一個長度為原來2倍(因為要滿足2的n次方)的陣列Node[],然後把舊陣列+連結串列上的元素全部轉移到新的Node[]上
連結串列長度為8的時候一定會轉成紅黑樹嗎?
不一定,如下程式碼:
if (tab == null || (n = tab.length) < 64) resize(); else //轉成紅黑樹
可以看到會先做判斷,滿足擴容條件會先擴容,不擴容再轉成紅黑樹。
(3)最終效果("haha8"put進去對應連結串列轉成了紅黑樹)
8.總結
Hashmap執行put的時候有以下幾個步驟: