目錄
一、分析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...