HashMap原始碼分析

2021-09-10 06:05:38 字數 4559 閱讀 2466

目錄

一、資料模型

二、重要屬性

三、構造方法

四、普通方法

put()

resize()

get()

五、總結

​ 在網上看過一些所謂的hashmap原始碼分析,大部分依舊是比較抽象的。究其原因,主要還是對hashmap的資料結構不理解。以下以圖示展示。

hashmap中存在乙個內部類:

static class nodeimplements map.entry
該內部類中,有四個屬性,我們儲存的是key和value。其中hash通過key計算的hashcode值,next是指向下乙個節點。由此可以理解儲存原理了。

/**

* the default initial capacity - must be a power of two.

*/// 思考:為什麼必須是2的倍數?

static final int default_initial_capacity = 1 << 4; // aka 16

// 最大長度

static final int maximum_capacity = 1 << 30;

int threshold;
threshold = capacity * loadfactor,當size>=threshold的時候,那麼就要考慮對陣列的擴增了,也就是說,這個的意思就是衡量陣列是否需要擴增的乙個標準。同時需要對應陣列上有元素。

static final float default_load_factor = 0.75f;
負載因子:該屬性是表示在擴容之前容量占有率的乙個標尺。它控制陣列存放node是的疏密程度。loadfactor越趨近於1,那麼陣列中存放的資料(entry)也就越多,也就越密,也就是會讓鍊錶的長度增加,loadfactor越小,也就是趨近於0,那麼陣列中存放的資料也就越稀。預設的0.75f是乙個權衡後的值。

static final int treeify_threshold = 8;
如果鍊錶超過了這個值,則將單鏈表轉變為紅黑樹。(1.8版本)

static final int untreeify_threshold = 6;
如果紅黑樹的節點被刪,且小於該閾值,則變為鍊錶。

transient int size;
記錄陣列已使用的容量,用來個閾值進行比較。

final float loadfactor;
填充因子

static final int min_treeify_capacity = 64;
桶中結構轉化為紅黑樹對應的table的最小大小

// 建構函式一

public hashmap()

// 建構函式二

public hashmap(int initialcapacity)

// 建構函式三

public hashmap(int initialcapacity, float loadfactor)

// 建構函式四

public hashmap(map extends k, ? extends v> m)

我們用的最多的就是第乙個構造方法,負載因子預設是0.75f。建構函式三是手動設定初始容量和負載因子,建構函式二呼叫的建構函式三,手動設定初始容量大小。建構函式是將別的map對映到自己的map中。

上面我們了解了hashmap的儲存結構。在給hashmap中新增元素的時候,我們應該考慮這樣乙個問題:如何確定元素的新增的位置?

我們來分析**:

public v put(k key, v value)
final v putval(int hash, k key, v value, boolean onlyifabsent,boolean evict) 

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

break;

p = e;}}

v oldvalue = e.value;

if (!onlyifabsent || oldvalue == null)

e.value = value;

afternodeaccess(e);

return oldvalue;}}

++modcount;

// 如果陣列中的元素個數超過閾值,則進行擴容

if (++size > threshold)

resize();

afternodeinsertion(evict);

return null;

}

通過以上的原始碼,我們可以看出。在新增新節點時,通過hash確定其位置。分以下種情況:

確定位置時,hash值起著重要的作用,我們看看該方法:

static final int hash(object key)
這裡通過位運算重新計算了hash值的值。為什麼要重新計算?

主要是因為n值比較小,hash只參與了低位運算,高位運算沒有用上。這就增大了hash值的碰撞概率。而通過這種位運算的計算方式,使得高位運算參與其中,減小了hash的碰撞概率,使hash值盡可能散開。

注意:我們在分析重要屬性default_initial_capacity的時候,陣列的初始化或者擴容為什麼給必須是2的n次冪?

在以上原始碼的(n - 1) & hash中給出了答案:(n - 1) & hash等價於對 length 取餘。但取餘的計算效率沒有位運算高,所以(n - 1) & hash也是乙個小的優化。例如,假設 hash = 185,n = 16。

這個方法我們在put()方法中已經呼叫過,作用是用來進行陣列的初始化和擴容。下面看看原始碼:

final node resize() 

// 容量翻倍,使用左移,效率更高

else if ((newcap = oldcap << 1) < maximum_capacity &&

oldcap >= default_initial_capacity)

// 閾值翻倍

newthr = oldthr << 1; // double threshold

}// 之前閾值大於0

else if (oldthr > 0)

newcap = oldthr;

// oldcap = 0並且oldthr = 0,使用預設值(如使用hashmap()建構函式,之後再插入乙個元素會呼叫resize函式,會進入這一步)

else

// 新閾值為0

if (newthr == 0)

threshold = newthr;

@suppresswarnings()

// 初始化table

node newtab = (node)new node[newcap];

table = newtab;

// 之前的table已經初始化過

if (oldtab != null)

else

} while ((e = next) != null);

if (lotail != null)

if (hitail != null) }}

}}return newtab;

}

其實仔細分析源**,基本上只要理解了hashmap的結構,其餘的邏輯也就和 清楚了。

public v get(object key)
final nodegetnode(int hash, object key)  while ((e = e.next) != null);}}

return null;

}

​ 一直都覺得hashmap的原始碼很簡單,仔細看看,其實不然。其中涉及了很多很多的知識點,比如資料結構就涉及了陣列、鍊錶和紅黑樹。其餘的細節都是數不勝數。細細研讀該原始碼一整天,都沒有將其中的細節詳盡解析。

1.資料結構是基礎,也是靈魂(通常伴隨演算法)。只有對資料結構了然於心,才能詳細了解底層原理;

2.伴隨jdk公升級的過程中,我們能很明顯感受到一些優化。比如將十進位制的值優化為二進位制的值;十進位制的計算優化為位運算;建構函式中,不給出長度,而是在新增第乙個元素的時候給出長度(預設長度或者設定的長度);

3.put()方法是hashmap的核心,弄清楚了這個方法,其餘的源**都不難;

4.讀原始碼一定要靜下心來,心不靜,就只能浮在表面;

5.個人水平和精力有限,難免有漏洞,希望大家多多指教。

HashMap原始碼分析

public hashmap int initialcapacity,float loadfactor 2 接下來是重要的put方法,put方法用於將鍵值對儲存到map中,讓我們來具體分析一下。public v put k key,v value if key null 若key為null,則將va...

HashMap 原始碼分析

1 getentry object key 方法 final entrygetentry object key return null 根據key的hash值計算出索引,得到table中的位置,然後遍歷table處的鍊錶 for entrye table indexfor hash,table.le...

HashMap原始碼分析

public v put k key,v value if key null return putfornullkey value int hash hash key int i indexfor hash,table.length for entrye table i e null e e.nex...