原文地址: https://jygod.github.io/2018/04/05/HashMap%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/
初始化
我們先來看看在初始化HashMap的時候會發(fā)生神馬:
HashMap<String, Person> map = new HashMap<String, Person>();
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
只會初始化一個參數(shù)loadFactor(負載因子),DEFAULT_LOAD_FACTOR 是負載因子的默認值0.75f意系。
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
那么我們可以知道初始化HashMap基本上什么都沒干...
然后我們進行下一個語句 Person p = new Person("Jiang", 18);
和 map.put("jy", p);
這是put()方法源碼:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
然后是putVal()方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else
...
... // 先過濾掉泥耀,反正剛開始不會往這里面走...
...
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
我們先來看看這里的table是什么?
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table;
相當(dāng)于是一個初始值為null的Node數(shù)組,再來看看Node的數(shù)據(jù)結(jié)構(gòu):
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
回到putVal()方法中...我們首次put元素的時候蛔添,這個table當(dāng)然是null的痰催,因此會執(zhí)行 table = resize();
resize()
方法就是HashMap進行動態(tài)擴容的關(guān)鍵方法。
由于resize()
方法有點長..我僅把首次擴容的關(guān)鍵代碼截下來...
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
...// 不會走這里
}
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY; // static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
..... // 不會走這里
}
return newTab;
}
我們很清楚的知道迎瞧,由于 table 是null夸溶,所以 oldCap = 0;所以有:
newCap = 1 << 4 = 16; newThr = 0.75f * 16 = 12;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
這兩行表示 table 現(xiàn)在就是正式在內(nèi)存堆里面開辟的大小為16的Node數(shù)組了~
現(xiàn)在的狀態(tài)差不多是介個樣子滴:
我們的元素此時還沒有加到map中凶硅,因為我們putVal()
方法還沒有分析完缝裁,只是對底層的table進行了初始化的擴容,接下來我們繼續(xù)分析putVal()
方法足绅,看看具體怎么把元素塞進map中的捷绑。
為了防止往上翻代碼....
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) // 剛才我們分析道這兒了!G饴琛粹污!
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // 現(xiàn)在從這兒開始...
tab[i] = newNode(hash, key, value, null);
else
...
... // 先過濾掉,反正剛開始不會往這里面走...
...
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
在resize()
擴容之后首量,通過 (n - 1) & hash 計算得到一個 0~15的下標值厕怜,得到的下標 i 正式我們元素應(yīng)該放入的table中的位置,如果計算后的i在table中的位置 table[i] 為null, 證明當(dāng)前位置還沒有任何節(jié)點元素粥航,我們需要new一個節(jié)點琅捏,并放在table[i]上。
所以現(xiàn)在的狀態(tài)應(yīng)該是這樣了QAQ:
而每一次put()
元素的時候递雀,如果在table[i]上元素為null柄延, 將元素插入table[i]時,都會進行size++ 的操作缀程, size對應(yīng)上圖HashMap對象的那個size:
if (++size > threshold)
resize();
因此搜吧,雖說在初始化時底層建立了一個16長度的數(shù)組,但是hashMap得邏輯長度最開始仍然是0杨凑, 只有在每次table[i]中有新元素到來的時候(并且table[i]為null)才會增加size;
而當(dāng)size也就是添加的元素個數(shù)超過閾值threshold滤奈,就會進行resize()
底層數(shù)組的擴容~ 這個我們之后再詳細說明。
HashMap的初始化操作就到這里了~
添加元素以及解決沖突
我們之前分析了向底層table添加新元素的過程撩满,現(xiàn)在我們執(zhí)行Person p1 = new Person("jiang", 20);
和map.put("jyjy", p1);
來模擬Hash沖突的情況蜒程。
還是先來分析putVal()方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
.... // 省略
....
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { // 這個時候應(yīng)該是走這里啦
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) // 如果發(fā)現(xiàn)要插入的元素的vale存在,則啥也不干..
e = p;
else if (p instanceof TreeNode) // 如果插入時發(fā)現(xiàn)是TreeNode結(jié)構(gòu)的
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // 轉(zhuǎn)換成紅黑樹前的插入操作
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
....
....
}
可以看到注釋里寫的伺帘,再插入時昭躺,有兩種可能的結(jié)構(gòu)。JDK1.7及以前版本中只會存在單鏈表的結(jié)構(gòu)伪嫁,而這種單鏈表的結(jié)構(gòu)领炫,一旦元素太多了,就會導(dǎo)致查詢效率低下... 因此在JDK1.8的版本中张咳,如果元素超過插入的閾值(8個)帝洪,就會將單鏈表轉(zhuǎn)換成紅黑二叉樹的結(jié)構(gòu),這里我們知道就好脚猾,具體的紅黑二叉樹轉(zhuǎn)換和插入比較復(fù)雜葱峡,就不在這篇文章中分析了...
那么我們需要關(guān)注的實際就是這一段代碼:
else { // 轉(zhuǎn)換成紅黑樹前的插入操作
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //在最后一個位置new一個新節(jié)點
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 超過閾值轉(zhuǎn)換成紅黑樹
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
這段代碼很好理解,從第一個節(jié)點開始向后遍歷婚陪,在鏈表的末尾插入新的節(jié)點,如果超過閾值(8個)频祝,則轉(zhuǎn)換成紅黑二叉樹泌参。
因此現(xiàn)在的狀態(tài)理論上是醬紫:
再來看最后執(zhí)行的這段:
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
插入節(jié)點或是發(fā)現(xiàn)已有一樣的節(jié)點后,e都不會為null(只有一開始table[i]為null的時候e才會是null)常空,所以上一段必定會執(zhí)行沽一。
因此會直接return這個插入節(jié)點的value,而不會執(zhí)行之后的size++;
擴容
在初始化的時候漓糙,我們看到putVal()這個方法里最后有這么一句話:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
....
....// 前面省略
if (++size > threshold)
resize();
}
初始的時候 threshold 的值我們計算得到的是 0.75f * 16 = 12铣缠,因此如果插入到table的元素超過閾值時(現(xiàn)在閾值為12)會觸發(fā)resize()對當(dāng)前table數(shù)組擴容。
又來到了resize()方法,只不過這一次就是真的要對底層數(shù)組進行擴容了~
final Node<K,V>[] resize() {
.....
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
}
現(xiàn)在oldCap是16 > 0蝗蛙,因此往下走蝇庭,newCap = 16 << 1 = 16 * 2 = 32,擴大兩倍捡硅。而閾值也是擴大兩倍哮内。
繼續(xù)往下面看...
final Node<K,V>[] resize() {
.....
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
...
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
}
是不是感覺有點長啊... 沒事,慢慢來… ..這一段是在干什么呢壮韭?在底層table數(shù)組擴容一倍之后北发,需要重新計算每個元素的放置位置,在JDK1.7及以前喷屋,其實都是根據(jù)rehash()重新計算節(jié)點的hash值然后再 e.hash & (oldCap - 1) 來重新計算節(jié)點的下標位置琳拨,進而進行重新的節(jié)點排列。
而JDK1.8優(yōu)化之后不需要再次對元素的hash值進行計算屯曹,而是只會將之前元素的hash值和oldCap值進行對比狱庇,觀察其最高位的0,1情況是牢,具體如下:
(e.hash & oldCap) 得到的是 元素的在數(shù)組中的位置是否需要移動,示例如下
示例1:
e.hash=10 0000 1010
oldCap=16 0001 0000
& =0 0000 0000 比較高位的第一位 0
結(jié)論:元素位置在擴容后數(shù)組中的位置沒有發(fā)生改變
示例2:
e.hash=17 0001 0001
oldCap=16 0001 0000
& =1 0001 0000 比較高位的第一位 1
結(jié)論:元素位置在擴容后數(shù)組中的位置發(fā)生了改變僵井,新的下標位置是原下標位置+原數(shù)組長度
將結(jié)果為0的存在以loHead為首的鏈表中, 將結(jié)果為1的存在以hiHead為首的鏈表中驳棱,然后分別放入table[i]和table[1 + oldCap]中批什。
這樣做能夠有效地將元素分散。