原文地址: 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的時(shí)候會(huì)發(fā)生神馬:
HashMap<String, Person> map = new HashMap<String, Person>();
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
只會(huì)初始化一個(gè)參數(shù)loadFactor(負(fù)載因子),DEFAULT_LOAD_FACTOR 是負(fù)載因子的默認(rèn)值0.75f。
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
那么我們可以知道初始化HashMap基本上什么都沒干...
然后我們進(jìn)行下一個(gè)語句 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
...
... // 先過濾掉,反正剛開始不會(huì)往這里面走...
...
++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)于是一個(gè)初始值為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元素的時(shí)候,這個(gè)table當(dāng)然是null的,因此會(huì)執(zhí)行 table = resize();
resize()方法就是HashMap進(jìn)行動(dòng)態(tài)擴(kuò)容的關(guān)鍵方法。
由于resize()方法有點(diǎn)長(zhǎng)..我僅把首次擴(kuò)容的關(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) {
...// 不會(huì)走這里
}
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) {
..... // 不會(huì)走這里
}
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)差不多是介個(gè)樣子滴:

我們的元素此時(shí)還沒有加到map中,因?yàn)槲覀?code>putVal()方法還沒有分析完,只是對(duì)底層的table進(jìn)行了初始化的擴(kuò)容,接下來我們繼續(xù)分析putVal()方法,看看具體怎么把元素塞進(jìn)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) // 剛才我們分析道這兒了?。?!
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) // 現(xiàn)在從這兒開始...
tab[i] = newNode(hash, key, value, null);
else
...
... // 先過濾掉,反正剛開始不會(huì)往這里面走...
...
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
在resize()擴(kuò)容之后,通過 (n - 1) & hash 計(jì)算得到一個(gè) 0~15的下標(biāo)值,得到的下標(biāo) i 正式我們?cè)貞?yīng)該放入的table中的位置,如果計(jì)算后的i在table中的位置 table[i] 為null, 證明當(dāng)前位置還沒有任何節(jié)點(diǎn)元素,我們需要new一個(gè)節(jié)點(diǎn),并放在table[i]上。
所以現(xiàn)在的狀態(tài)應(yīng)該是這樣了QAQ:

而每一次put()元素的時(shí)候,如果在table[i]上元素為null, 將元素插入table[i]時(shí),都會(huì)進(jìn)行size++ 的操作, size對(duì)應(yīng)上圖HashMap對(duì)象的那個(gè)size:
if (++size > threshold)
resize();
因此,雖說在初始化時(shí)底層建立了一個(gè)16長(zhǎng)度的數(shù)組,但是hashMap得邏輯長(zhǎng)度最開始仍然是0, 只有在每次table[i]中有新元素到來的時(shí)候(并且table[i]為null)才會(huì)增加size;
而當(dāng)size也就是添加的元素個(gè)數(shù)超過閾值threshold,就會(huì)進(jìn)行resize()底層數(shù)組的擴(kuò)容~ 這個(gè)我們之后再詳細(xì)說明。
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 { // 這個(gè)時(shí)候應(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) // 如果插入時(shí)發(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;
}
}
....
....
}
可以看到注釋里寫的,再插入時(shí),有兩種可能的結(jié)構(gòu)。JDK1.7及以前版本中只會(huì)存在單鏈表的結(jié)構(gòu),而這種單鏈表的結(jié)構(gòu),一旦元素太多了,就會(huì)導(dǎo)致查詢效率低下... 因此在JDK1.8的版本中,如果元素超過插入的閾值(8個(gè)),就會(huì)將單鏈表轉(zhuǎn)換成紅黑二叉樹的結(jié)構(gòu),這里我們知道就好,具體的紅黑二叉樹轉(zhuǎn)換和插入比較復(fù)雜,就不在這篇文章中分析了...
那么我們需要關(guān)注的實(shí)際就是這一段代碼:
else { // 轉(zhuǎn)換成紅黑樹前的插入操作
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) { //在最后一個(gè)位置new一個(gè)新節(jié)點(diǎn)
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;
}
}
這段代碼很好理解,從第一個(gè)節(jié)點(diǎn)開始向后遍歷,在鏈表的末尾插入新的節(jié)點(diǎn),如果超過閾值(8個(gè)),則轉(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é)點(diǎn)或是發(fā)現(xiàn)已有一樣的節(jié)點(diǎn)后,e都不會(huì)為null(只有一開始table[i]為null的時(shí)候e才會(huì)是null),所以上一段必定會(huì)執(zhí)行。
因此會(huì)直接return這個(gè)插入節(jié)點(diǎn)的value,而不會(huì)執(zhí)行之后的size++;
擴(kuò)容
在初始化的時(shí)候,我們看到putVal()這個(gè)方法里最后有這么一句話:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
....
....// 前面省略
if (++size > threshold)
resize();
}
初始的時(shí)候 threshold 的值我們計(jì)算得到的是 0.75f * 16 = 12,因此如果插入到table的元素超過閾值時(shí)(現(xiàn)在閾值為12)會(huì)觸發(fā)resize()對(duì)當(dāng)前table數(shù)組擴(kuò)容。
又來到了resize()方法,只不過這一次就是真的要對(duì)底層數(shù)組進(jìn)行擴(kuò)容了~
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,擴(kuò)大兩倍。而閾值也是擴(kuò)大兩倍。
繼續(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;
}
}
}
}
}
}
是不是感覺有點(diǎn)長(zhǎng)啊... 沒事,慢慢來… ..這一段是在干什么呢?在底層table數(shù)組擴(kuò)容一倍之后,需要重新計(jì)算每個(gè)元素的放置位置,在JDK1.7及以前,其實(shí)都是根據(jù)rehash()重新計(jì)算節(jié)點(diǎn)的hash值然后再 e.hash & (oldCap - 1) 來重新計(jì)算節(jié)點(diǎn)的下標(biāo)位置,進(jìn)而進(jìn)行重新的節(jié)點(diǎn)排列。
而JDK1.8優(yōu)化之后不需要再次對(duì)元素的hash值進(jìn)行計(jì)算,而是只會(huì)將之前元素的hash值和oldCap值進(jìn)行對(duì)比,觀察其最高位的0,1情況,具體如下:
(e.hash & oldCap) 得到的是 元素的在數(shù)組中的位置是否需要移動(dòng),示例如下
示例1:
e.hash=10 0000 1010
oldCap=16 0001 0000
& =0 0000 0000 比較高位的第一位 0
結(jié)論:元素位置在擴(kuò)容后數(shù)組中的位置沒有發(fā)生改變
示例2:
e.hash=17 0001 0001
oldCap=16 0001 0000
& =1 0001 0000 比較高位的第一位 1
結(jié)論:元素位置在擴(kuò)容后數(shù)組中的位置發(fā)生了改變,新的下標(biāo)位置是原下標(biāo)位置+原數(shù)組長(zhǎng)度
將結(jié)果為0的存在以loHead為首的鏈表中, 將結(jié)果為1的存在以hiHead為首的鏈表中,然后分別放入table[i]和table[1 + oldCap]中。
這樣做能夠有效地將元素分散。