1. 程式人生 > 實用技巧 >HashMap put原理詳解(基於jdk1.8)

HashMap put原理詳解(基於jdk1.8)

此文轉載自:https://blog.csdn.net/weixin_49631226/article/details/110247453

前言

本文是個人對Hashmap的一些個人見解,主要通過使用hashmapput的一些程式碼來闡述其底層實現原理,在面試中也會經常會用到,如有不對的地方望大家指正。

(1)先描述一下hashmap的一個底層資料結構:

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[]陣列的初始長度(容量)的參考,

預設為16。

可以自己指定長度,指定方式為: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)

hahaput結果

3.put不同key不同index的值:map.put("heihei","heihei的值");

這裡的過程和2.一樣,最終結果為(這裡設"heihei"算出的index為0):

heiheiput結果

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,那就直接覆蓋原值就行了,最終結果為:

haha另一個值 put結果

5.首次put不同key相同index的值:map.put("haha2","haha2的值")

這裡假設"haha2"算出來的index和"haha"一樣。

在這個前提下,自然就會出現雜湊碰撞。這時候的過程也是和4.一樣,區別在於在比對key的時候,發現兩個key的hash值不相等,也就不是同一個key

之後自然會把這個元素插到"haha"元素後面形成連結串列

插入的方式很簡單,就是把"haha2"元素賦給"haha"的next,"haha2"的next為null

最終結果為:

haha2 put結果

這裡插入之前會進行一些判斷,比如是否是紅黑樹等等,後面的情況會細講。

6.繼續put不同key相同index的值:map.put("haha3","haha3的值")

這裡假設"haha3"算出來的index和"haha"一樣。

過程和5.一樣,區別在於在比對的時候,會遍歷當前陣列位置上鍊表的所有元素,如果發現相同key則覆蓋,沒有相同key就追加到連結串列後面。

最終結果為:

haha3 put結果

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進去對應連結串列轉成了紅黑樹)

haha8 put結果

8.總結

Hashmap執行put的時候有以下幾個步驟:

(1)先計算出對應key的hash值,然後去判斷當前Node[]陣列是不是為空,為空就新建,不為空就對hash值作減一與運算得到陣列下標

(2)然後會判斷當前陣列位置有沒有元素,沒有的話就把值插到當前位置,有的話就說明遇到了雜湊碰撞

(3)遇到雜湊碰撞後,就會看下當前連結串列是不是以紅黑樹的方式儲存,是的話,就會遍歷紅黑樹,看有沒有相同key的元素,有就覆蓋,沒有就執行紅黑樹插入

(4)如果是普通連結串列,則按普通連結串列的方式遍歷連結串列的元素,判斷是不是同一個key,是的話就覆蓋,不是的話就追加到後面去

(5)當連結串列上的元素達到8個的時候,如果不滿足擴容條件,連結串列會轉換成紅黑樹;如果滿足擴容條件,則hashmap會進行擴容,把容量擴大到以前的兩倍