HashMap底層原始碼解析

2021-09-27 03:38:50 字數 3237 閱讀 6901

目錄

一、分析hashmap的資料結構

1.使用陣列儲存,加快訪問速度

2.陣列中的鍊錶,解決hash衝突

3.使用紅黑樹優化鍊錶,防止大量hash衝突

二、hashmap主要原始碼解讀

三、總結

在看原始碼之前,了解一下它的資料結構和執行過程,才能更快更加有效率的讀懂原始碼。

hashmap實際儲存的是乙個陣列

transient node table;
使用陣列儲存的好處有很多,最大的特點就是陣列的訪問速度相當快。

每當新增/獲取元素時,hashmap會根據key值的hashcode,計算出當前key對應的鍵值對物件(node.class)存放在陣列的哪個索引位置上。

這個陣列儲存型別的是乙個引用型別,在hashmap的內部類中,可以看一下它的原始碼(精簡版)。

static class nodeimplements map.entry

}

成員變數中除了儲存著k,v,hash之外,還有乙個node變數,也就是他自己。這種組成方式,也是一種資料結構,叫做鏈。

鍊錶

雖然陣列的訪問速度相當快,但是他的大小是固定不變的,而且每次插入元素和刪除元素,其他的元素也要跟著一起發生變化,這對於經常插入或者刪除元素的應用場景來說簡直就是災難。

雖然鍊錶的儲存不連續,而且大小也不是一樣的,但是只要根據引用一級一級向下查詢,就能夠找到你想要的任何元素。

關於鏈和陣列的區別和優缺點在此就不多贅述了,大家在看arraylist和linkedlist的區別的時候就基本上都看過了。

由上可知hashmap它的底層儲存結構為:儲存著鍊錶的陣列

通過陣列和鍊錶的雙重結構,我們的hashmap已經可以說是很完善了,

但是如果發生大量的hash碰撞,就會導致一條甚至多條鏈過長的情況,這個時候由於鍊錶的特性,會導致其獲取元素的效率降低。

此時,使用樹狀儲存結構將有效的解決該問題。

那麼知道了hashmap的資料結構,可以進一步開始讀取原始碼。

ps:不想看原始碼就直接跳到文章末尾看總結吧。

hashmap**的核心主要就是儲存這一塊了,在知道了hashmap的儲存結構之後,我們大概就能猜到它的**的流程了,這裡說一下put()方法的流程(不包含hashmap的一些優化策略):

根據鍵值對(之後用k,v表示),計算出k的hash碼(之後用hash表示)。

用hash和陣列table的長度-1進行與運算,得到當前的鍵值對對應的陣列索引位置。

如果陣列的這個索引對應值為null,說明這裡沒有鏈,直接將用鍵值對構建乙個node物件,儲存到陣列該索引處。

如果不為空,說明此處已經有元素了,此時發生了hash碰撞,那麼應該遍歷這個鍊錶,將該鍵值對新增到鍊錶的末尾。

現在再來看原始碼吧,put()方法直接呼叫了putval()方法

public v put(k key, v value)
hash碼的運算,進行了位運算,右移了16位(這裡的意義是什麼?求大佬指出)

問題已經解決,為什麼要存在hash方法而不是直接使用hashcode()

static final int hash(object key)
接著直接開始就是新增元素了,**比較長,我們到後面一步一步拆解

1.計算得出該鍵值對應該存放的索引位置

hashmap第一次進行初始化是在第一次新增元素時,預設的陣列長度時1<<4=16,預設的負載因子是0.75,也就是說,存放的元素超過陣列的0.75就會進行重新擴容2倍(也就是new乙個新的陣列,長度=oldlength*2);

final v putval(int hash, k key, v value, boolean onlyifabsent,

boolean evict)

2.當發生hash碰撞時,如何處理

如果發生hash碰撞,首先判斷key值是否相等,如果相等則覆蓋舊值,如果不等則將該鍵值對新增到鍊錶末尾。

在新增元素之後,需要判斷鍊錶的長度(1.8版本之後),如果hashmap的長度大於等於8,則會將鍊錶轉換成紅黑樹儲存(樹狀儲存結構在查詢和刪除上花費的時間更少)。

//省略上面的**

//同上,這裡也是判斷k值重複操作

if (e.hash == hash &&

((k = e.key) == key || (key != null && key.equals(k))))

break;

p = e;}}

//有上面的**可以知道,當成功新增之後,e=null值,

//如果k值相同,e對應的就是那個k相同的node索引

v oldvalue = e.value;

//因為onlyifabsent是寫死的false,所以此處一定會將舊的value值替換成新鍵值對的value值

if (!onlyifabsent || oldvalue == null)

e.value = value;

//預設配置是啥也沒乾,該方法是為了hashmap的子類實現的

afternodeaccess(e);

//返回舊的value值

return oldvalue;}}

//省略**

3.插入成功後,判斷是否需要擴容

//下面三個都是成員變數

++modcount;

//當map的大小足夠大時,此時需要擴容

if (++size > threshold)

resize();

//預設啥也沒乾,該方法是為了hashmap的子類實現的

afternodeinsertion(evict);

return null;

看到這裡呢,整個hashmap的核心基本上也就看完了。

總結一下就是:

HashMap底層原始碼剖析

陣列 單向鍊錶 紅黑樹 陣列 陣列每一項都是乙個鍊錶,其實就是陣列和鍊錶的結合體 單向鍊錶 當法神hash碰撞時,首先會找到陣列對應位置,然後1.8採用尾插入法 1.7採用頭插入法 形成乙個單項鍊表結構 紅黑樹 當陣列中每項的鍊錶長度大於8時,會轉換為紅黑樹 hash碰撞 不同的key可能會產生相同...

HashMap底層原始碼實現

首先需要明確的是 hashmap 內部結構 可以看作是陣列和鍊錶結合組成的復合結構,陣列被分為乙個個桶 bucket 每個桶儲存有乙個或多個entry物件,每個entry物件包含三部分 key 鍵 value 值 next 指向下乙個entry 通過雜湊值決定了entry物件在這個陣列的定址 雜湊值...

HashMap底層原始碼分析

static final int default initial capacity 1 4 aka 16表示1向左移4位,2的4次方 static final int maximum capacity 1 30 hashmap陣列的最大容量 static final float default lo...