HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

碼蟻小強發表於2021-05-10

各位同學大家好, 今天給大家分享一下HashMap內部的實現原理, 這一塊也是在面試過程當中基礎部分被問得比較多的一部分。

想要搞清楚HashMap內部的實現原理,我們需要先對一些基本的概念有一些瞭解, 這些概念包括什麼是hash、什麼是hash表、什麼是hashcode? 有了這些基本概念之後, 我們再去分析Hashmap,就相對來講簡單了一些。

什麼是Hash

雜湊(hash)簡單理解就是將任意長度的輸入通過雜湊演算法轉換成固定長度的輸出,建立一種一一對應的關係.這個輸出一般稱之為雜湊碼或雜湊值。

常見hash演算法

上面是文字概念如果你還不是太明白的話, 接下來列舉一些常見的hash演算法:

比如我們的MD5加密,假設你的密碼為1234通過MD5加密後就變成了
81dc9bdb52d04dc20036dbd8313ed055這樣一串加密。1234和81dc9bdb52d04dc20036dbd8313ed055就建立了一種對應的關係。 1234今後使用MD5加密得到的結果就是固定的81dc9bdb52d04dc20036dbd8313ed055值,得到的這個值我們稱為hash值。

再比如我們的ASSIC碼錶,它也是一種hash演算法,我們的一個字元'A'與數字65建立的這一種對映關係。我們的'A' 通過hash演算法後,總是能找到固定數字 65,這個65我們就稱它是Hash值。

Hash表

雜湊表(Hash table,也叫雜湊表),是根據關鍵碼值(Key value)而直接進行訪問的資料結構。也就是說,它通過把關鍵碼值對映到表中一個位置來訪問記錄,以加快查詢的速度。

如果我們想要儲存多個字母, 儲存多個值. 在Java當中可以使用陣列來實現。如果我們直接把字母按照陣列的角標進行儲存的話, 按照如下方式進行儲存。

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

假設此時我們想要獲取到b的值 , 我們就須要先遍歷,取出每一個元素判斷是否等於b.這樣效率就比較低。

現在就可以結合上面的上面講的hash值來進行儲存, 每一個字元都會有一個assic碼的值, 我們可以獲取該字母的assic值進行儲存. 放到物件assic碼值所在的角標位置,如下圖:

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

通過這種方式儲存,我們假設想要獲取的內容,就只需要先算出a的assic碼值97 在取的時候的時候直接arr[97] 即可直接取出對應位置的字碼.無需再遍歷陣列進行比較。 這樣的一個陣列,根據關鍵碼值(Key value)直接進行訪問的資料結構,我們就稱為是hash表。

HashCode

在上面儲存字母a的時候,有同學可能會有疑問, 假設我的陣列只有16個空間大小儲存範圍只有0到15. 字母a的assic碼是97,直接進行儲存的話, 不會是發生異常嗎? 的確會是有這樣的問題,所以我們在拿到hash值後, 並不是立即進行儲存. 在這裡我們先對獲取的hash碼值%16(16就是我們陣列的長度),除以16取餘數,這樣就可以把範圍固定在0-15之間,97%16得到結果1. 我們就把字母a存到1角標的位置arr[1]=a , 在取的時候我們使用同樣的方式先獲取a的assic碼再%16 就可以直接獲取到對應的值.如下圖:

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

在這裡面計算出來的數字1 也就是在陣列當中儲存的角標,我們就可以稱它是a的hashcode碼,hashcode碼就是在hash表中有對應的位置.

HashCode的存在主要是為了查詢的快捷性,HashCode是用來在雜湊儲存結構中確定物件的儲存地址的.

Object類中的hashCode方法

Java語言中,JVM每new一個Object,它都會將這個Object丟到一個Hash雜湊表中去,這樣的話,下次做 Object的比較或者取這個物件的時候,它會依據物件的hashcode再從Hash表中取這個物件。這樣做的目的是提高取物件的效率。Object物件有個特殊的方法:hashcode() 此方法作用是到獲取對應的hashcode碼值。

HashMap內部資料結構:

有了上面的知識儲備之後, 我們再來看HashMap的原理實現.hashmap內部的實現原理在JDK1.7與1.8當中有一個大的改變.

JDK1.7中使用的資料結構是:陣列+連結串列

JDK1.8中使用的資料結構是:陣列+連結串列+紅黑樹

接收下來我們先以JDK1.7當中的實現來去給大家講,JDK1.7明白之後,1.8是在1.7的基本上加了一個紅黑樹。

HashMap實現原理

我們先來看一下hashMap的基本使用,如下圖:

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

其中put方法中第一個引數為key(key必須是引用資料型別),第二個引數為value。 這一組key:value 我們稱之為Entry,在hashmap中對應的是HashMap當中一個內部類,如下圖:

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

我們每put一組key和value的時候, 就會給我們建立一個Entry物件. 把建立的entry物件放到陣列當中去.在hashMap原始碼中我們可以看到如下的一些屬性,其中就定義了陣列的初始容量和空的陣列。

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

我們的Entry物件會被放到table這個陣列當中,如果下圖.

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

在put一個元素時, 會先判斷陣列是否為空, 如果陣列為空就去建立陣列.

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

第26行位置幫我們初始化陣列.從put方法的原始碼中我們可以看到, 內部的陣列是懶載入的. 我們進入到該方法中檢視

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

Entry物件往陣列當中進行存放的時候,並不是直接按角標的形式進行存放. 採用hash表的形式進行儲存。

在put元素時,entry對的的key必須一個引用資料型別. 引用資料型別在使用的時候可以呼叫hashcode()這個方法. 返回的是一串int型別的數字. 它這串數字當作是hash值進行儲存到陣列當中的物件位置. 這串數字比較大, 陣列的儲存空間大小隻有16. 所以要對該資料進行取模 %16把結果限制在0-15之間(注:hashmap當中並不是直接取模,而是採用的位運算達到同樣目的,這點後面介紹). 過程如下圖:

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

在儲存的過程當中可能會發生hash碰撞

什麼是hash碰撞

所謂hash碰撞指的是經過hash演算法之後, 計算出的hash碼是同一個值,如下圖:

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

在hashmap中為了解決這種情況,引入了連結串列,也就是我們在上面看Entry內部類宣告的時候有一有個next的屬性.如果產生了hash衝突,就會以連結串列頭插發的形式來記錄對應的資料.

舉例: 假設現在角標9的位置被張三實體佔用,是第一個放到角標9的位置

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

下一次再存放李四,假設李四經過hash之後,得到的位置也是9

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

會採用連結串列來解決hash衝突

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

後面如果再產生hash衝突,會遍歷連結串列,檢視有沒有相同的entry物件,如果有,則進行更新操作,如果沒有的話, 把該物件插入到連結串列頭部. 如果沒有產生hash衝突,直接存放到對應角標.

以下為put方法原始碼:

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

本篇內容就先介紹到這裡, 關於hashmap的內容裡面還有很多, 本篇內容主要先對hashmap內部的陣列+連結串列有一個全域性的認識. 裡面還有很多問題我們還沒有去分析. 比如:為什麼陣列的長度一定要為2冪次方, 新增元素時擴容的問題, 在1.7當中擴容轉多資料時,多執行緒情況下為什麼可能會產生cpu使用率100%的情況?. jdk1.8中加入了紅黑樹是什麼意思?這些將會在下篇文章當中給大家一個一個的進行分析.

對於這部分的內容我也錄製了相應的詳細視訊講解,想要獲取視訊的同學可以在討論區當中留言哈,

HashMap實現原理一步一步分析(1-put方法原始碼整體過程)

 

如果這篇文章對你有幫助的話,可以轉發分享給身邊的小夥伴哦! 感謝觀看!

相關文章