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


各位同學大家好, 今天給大家分享一下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方法源碼整體過程)

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM