
本文原創(chuàng)地址,
我的博客:https://jsbintask.cn/2019/02/27/jdk/jdk8-hashmap-sourcecode/(食用效果最佳),轉(zhuǎn)載請(qǐng)注明出處!
前言
前陣子(估計(jì)也快半年了吧)遇到這么一個(gè)面試題:請(qǐng)一行代碼一行代碼描述下HashMap put方法。
我:。。。
哈哈,其實(shí)也沒有無(wú)語(yǔ),當(dāng)時(shí)知道HashMap的原理,數(shù)據(jù)結(jié)構(gòu),以及一些要注意的點(diǎn),沒想到面試官這么狠,所以本文的目的就是全方位的從源碼角度分析下HashMap。
注意點(diǎn)
jdk1.8相對(duì)于jdk1.7有較大改動(dòng),本次將只會(huì)詳細(xì)分析jdk1.8的代碼,對(duì)于1.7只會(huì)比較兩者不同之處。
源碼
數(shù)據(jù)結(jié)構(gòu)
jdk1.8以前,HashMap使用的是數(shù)組+鏈表的結(jié)構(gòu)存儲(chǔ)數(shù)據(jù)。如下:

維護(hù)一個(gè)
Enyry[]數(shù)組存儲(chǔ)數(shù)據(jù),當(dāng)發(fā)生hash沖突時(shí),數(shù)組節(jié)點(diǎn)則會(huì)變成一個(gè)鏈表,用于存儲(chǔ)hash沖突的數(shù)據(jù),而在jdk1.8中,這個(gè)鏈表則變成了紅黑樹,如下:
值得注意的是,jdk8中不是一開始就使用
紅黑樹維護(hù)這些hash沖突的節(jié)點(diǎn),而是當(dāng)鏈表長(zhǎng)度超過某個(gè)閾值時(shí)才將鏈表轉(zhuǎn)換為紅黑樹,在代碼分析中看到。
源碼分析
- 首先查看
HashMap中的靜態(tài)成員常量
public class HashMap {
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
}
各個(gè)靜態(tài)常量如圖:

各個(gè)靜態(tài)成員常量意義已經(jīng)注明.
-
HashMap成員變量,接著,我們看下成員變量:
public class HashMap<K, V> {
/**
* 一開分析的 `儲(chǔ)存數(shù)據(jù)的數(shù)組`
*/
transient java.util.HashMap.Node<K,V>[] table;
/**
* 用于 **entrySet()和values()**方法,返回一個(gè)迭代器遍歷Map結(jié)構(gòu)
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 整個(gè)hashmap 所包含的節(jié)點(diǎn)數(shù)
*/
transient int size;
/**
* hashmap 的結(jié)構(gòu)修改次數(shù),比如 Put,remove的次數(shù),
* 和上面的 迭代器配合使用,在迭代過程中,如果其它線程更改了這個(gè)值,則會(huì)拋出 `ConcurrentModificationException`異常
*/
transient int modCount;
/**
* hashmap擴(kuò)容的閾值,值為 loadFactor*table.length e.g: 0.75 * 16 = 12
* 也就是說默認(rèn)是當(dāng)數(shù)組大小超過 12時(shí)就會(huì)進(jìn)行數(shù)組擴(kuò)容
*/
int threshold;
/**
* 加載因子,默認(rèn)值上圖已經(jīng)說明
*/
final float loadFactor;
}
其中各個(gè)值的含義已經(jīng)在注釋中說明,配合上圖默認(rèn)值更能理解。
- 接著我們繼續(xù)看下
Node類的數(shù)據(jù)結(jié)構(gòu):
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
很清楚,四個(gè)成員變量, key, key的hash值, key對(duì)應(yīng)的value,下一個(gè)節(jié)點(diǎn)的引用,其中鏈表的形成就是 next這個(gè)引用的作用。
- 好了,準(zhǔn)備條件都做好了,接下來就是分析
put方法了:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
很清楚,通過hash(key)方法獲取到了key的hash值,然后調(diào)用了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 {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
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;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
這是putVal的原始方法,看起來有點(diǎn)復(fù)雜,很多操作在一行代碼中寫完,我們稍微改下寫法,為每行代碼加上注釋:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
/* 聲明本地變量 tab,p,n,i(提高性能,effective java),可以先多記兩邊,防止后面不知道變量怎么來的! */
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
/* 將成員變量 table 賦值給本地變量 tab,并且將tab的長(zhǎng)度賦值給本地變量 n */
tab = table;
if (tab != null) {
n = tab.length;
}
/* 如果tab為空或者 數(shù)組長(zhǎng)度為0,進(jìn)行初始化,調(diào)用 resize()方法,并且獲取賦值后的數(shù)組長(zhǎng)度 */
if (tab == null || n = 0) {
tab = resize();
n = tab.length;
}
/* 根據(jù)key的hash值得到當(dāng)前key在數(shù)組中的 位置,賦值給 i */
i = (n - 1) & hash;
/* 將i在數(shù)組中對(duì)應(yīng)的key值去除賦值給p,所以p代表當(dāng)前的key */
p = tab[i];
/* 判斷當(dāng)前數(shù)組中取出來的key是否為空(數(shù)組中沒有),就new一個(gè)新的節(jié)點(diǎn),并且放在這個(gè)索引 i的位置 */
if (p == null) {
tab[i] = newNode(hash, key, value, null);
/* 如果不為空,那就表示已經(jīng)有這樣的hash 值已經(jīng)存在了,可能存在hash沖突 或者 直接替換原來的value */
} else {
/* 聲明本地變量 e, k */
Node<K, V> e;
K k;
/* 如果取出來的節(jié)點(diǎn) hash值相等,key也和原來的一樣( == 或者 equals方法為true),直接將 這個(gè)節(jié)點(diǎn)
* p 賦值給剛剛聲明的本地變量 e (這個(gè)操作很重要,在心中記?。? * 另外這里還將 節(jié)點(diǎn) p的key 賦值給了本地變量 k
* */
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
e = p;
/* 如果 hash值一樣,但不是同一個(gè) key,則表示hash沖突,接著判斷這個(gè)節(jié)點(diǎn)是不是 紅黑樹的節(jié)點(diǎn)
* 如果是,則生成一個(gè)紅黑樹的節(jié)點(diǎn)然后賦值給本地變量 e */
} else if (p instanceof TreeNode) {
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
/* 不是紅黑樹,hash沖突了,這個(gè)時(shí)候開始擴(kuò)展鏈表 */
} else {
/* 聲明一個(gè)本地變量 binCount,開始遍歷 p節(jié)點(diǎn)后面的鏈表 */
for (int binCount = 0; ; ++binCount) {
/* 首先將p節(jié)點(diǎn)的 next(鏈表的下一個(gè))賦值給 本地變量e */
e = p.next;
/* 如果e為空,表示p指向的下一個(gè)節(jié)點(diǎn)不存在,這個(gè)時(shí)候直接將 新的 key,value放在鏈表的最末端 */
if (e == null) {
p.next = newNode(hash, key, value, null);
/* 放入后,還要判斷下 這個(gè)鏈表的長(zhǎng)度是否已經(jīng)大于等于紅黑樹的閾值 (前面分析靜態(tài)成員變量已經(jīng)說明),
* 一旦大于,就可以變形,調(diào)用 treeifyBin方法將原來的鏈表轉(zhuǎn)化為紅黑樹 !
* */
if (binCount >= TREEIFY_THRESHOLD - 1) { // -1 for 1st
treeifyBin(tab, hash);
}
break;
}
/* 如果不為空,表示還沒有到鏈表的末端,
將 e 賦值給 p(p的下一個(gè)節(jié)點(diǎn)賦值給p),開啟下一次循環(huán) */
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
break;
}
p = e;
}
}
/* e不等于null,則表示 key值相等,替換原來的value即可,
* 這里需要注意,這里不是表示 hash沖突(再觀察下前面的分析),
* hash沖突鏈表的擴(kuò)展已經(jīng)在最后一個(gè) else完成了!
* */
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) {
e.value = value;
}
/* 替換新值后,回調(diào)該方法(子類可擴(kuò)展) */
afterNodeAccess(e);
/* 返回原來的 key對(duì)應(yīng)的舊值 */
return oldValue;
}
}
/* 完成一次 put方法后,加一次 modCount,看前面成員變量分析 */
++modCount;
/* 加入了新節(jié)點(diǎn),把 size 自加,并且 判斷是否已經(jīng)大于要擴(kuò)容的閾值(觀察前面成員變量分析),開始擴(kuò)容 */
if (++size > threshold)
resize();
/* 插入新節(jié)點(diǎn)后,回調(diào)方法(子類可擴(kuò)展) */
afterNodeInsertion(evict);
/* 插入的新節(jié)點(diǎn),直接返回 null即可 */
return null;
}
其中所有代碼均已經(jīng)加上詳細(xì)注釋,這里值得注意的是,由于這個(gè)方法沒有任何 線程同步手段,所以不論是在查找對(duì)應(yīng)的key,還是擴(kuò)容,插入節(jié)點(diǎn),增加size,modCount等,肯定會(huì)出現(xiàn)問題(這里先預(yù)留一篇文章,ConCurrentHashMap源碼分析),所以多線程環(huán)境下,絕對(duì)不能使用HashMap,
而應(yīng)該使用ConCurrentHashMap。
當(dāng)然到了現(xiàn)在,我們那個(gè)面試題的答案也已經(jīng)能夠較為完整的回答出來了!(大笑)
- 上面較為詳細(xì)的分析了put方法后,我們注意到
resize()方法在這個(gè)方法中起到了關(guān)鍵作用,初始化,以及擴(kuò)容。那我們接著來觀察下resize()方法:
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) {
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
} else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float) newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes", "unchecked"})
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;
}
}
}
}
}
return newTab;
}
這個(gè)方法看起來也較為復(fù)雜,我們同樣作下簡(jiǎn)單分析:
final Node<K, V>[] resize() {
/* 同樣聲明本地變量,得到原來的數(shù)組,提高性能 */
Node<K, V>[] oldTab = table;
/* 獲得數(shù)組的長(zhǎng)度 */
int oldCap = (oldTab == null) ? 0 : oldTab.length;
/* 獲取擴(kuò)容閾值 */
int oldThr = threshold;
/* 新的數(shù)組長(zhǎng)度,新的閾值 */
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
/* 將原來的數(shù)組長(zhǎng)度 * 2 判斷是否小于最大值,并且原來的數(shù)組長(zhǎng)度大于 默認(rèn)初始長(zhǎng)度(16)
* 直接雙倍擴(kuò)容, 閾值,長(zhǎng)度都 * 2
* */
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
} else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
/* 第一次調(diào)用 resize方法,初始化數(shù)組長(zhǎng)度,閾值,這里就對(duì)應(yīng)我們前面成員變量的分析了:
* 閾值 = 加載因子 * 數(shù)組長(zhǎng)度
* */
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float) newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE);
}
threshold = newThr;
/* 根據(jù)前面計(jì)算出來的新長(zhǎng)度,聲明一個(gè)新數(shù)組 */
@SuppressWarnings({"rawtypes", "unchecked"})
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
table = newTab;
/* 開始將舊數(shù)組的長(zhǎng)度復(fù)制到新數(shù)組 */
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
if ((e = oldTab[j]) != null) {
/* 原數(shù)組的值先置換為null,幫助gc */
oldTab[j] = null;
/* 如果節(jié)點(diǎn)的next不為空(沒有形成鏈表),直接復(fù)制到新數(shù)組 */
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
/* 不為空但是已經(jīng)是 紅黑樹了,按紅黑樹規(guī)則置換 */
else if (e instanceof TreeNode)
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
/* 已經(jīng)形成鏈表了,循環(huán)將鏈表的引用到新數(shù)組,不再使用鏈表 */
else { // preserve order
/* 聲明四個(gè)引用,可以防止多線程環(huán)境下 死循環(huán)! */
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;
}
}
}
}
}
/* 最后返回新數(shù)組 */
return newTab;
}
注釋已經(jīng)簡(jiǎn)要說明流程,這里可以看出有數(shù)組復(fù)制以及重新計(jì)算hash的操作,所以我們?cè)趯?shí)際開發(fā)中使用HashMap的時(shí)候,最好設(shè)置一個(gè)初始容量,防止經(jīng)常擴(kuò)容操作耗費(fèi)性能!
- 好了,
HashMap兩個(gè)關(guān)鍵方法都分析完畢了,接下來我們最后分析一個(gè)方法,get(key):
public V get(Object key) {
Node<K, V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
get方法首先通過 hash(key)方法獲取到了hash值,接著通過getNode(hash)方法獲取節(jié)點(diǎn),所以我們重點(diǎn)看下getNode方法:
final Node<K, V> getNode(int hash, Object key) {
/* 聲明本地變量,提高性能 */
Node<K, V>[] tab;
Node<K, V> first, e;
int n;
K k;
/* 本地變量賦值,n為數(shù)組長(zhǎng)度 */
tab = table;
if (tab != null) {
n = tab.length;
}
/* 通過 hash值算出key在數(shù)組中的 位置,取出該節(jié)點(diǎn) */
first = tab[(n - 1) & hash];
/* 不為空,表示key在數(shù)組中存在,接下來開始遍歷鏈表獲取紅黑樹,找出具體位置 */
if (tab != null && n > 0 && first != null) {
/* 如果鏈表或者紅黑樹的第一個(gè)節(jié)點(diǎn) hash值,key相等,這個(gè)節(jié)點(diǎn)就是我們要找的,直接返回 */
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
/* 開始遍歷鏈表 */
if ((e = first.next) != null) {
/* 如果是紅黑樹,直接按樹規(guī)則 查找然后返回 */
if (first instanceof TreeNode)
return ((TreeNode<K, V>) first).getTreeNode(hash, key);
do {
/* 遍歷鏈表找到了,返回 */
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
/* 最后沒有找到,直接返回null */
return null;
}
所有代碼均已經(jīng)加上詳細(xì)注釋,這里值得注意的是, 我們發(fā)現(xiàn)在鏈表中查找節(jié)點(diǎn)采用的是遍歷的方式,所以一旦鏈表過長(zhǎng),查找性能就較慢,這也是為什么jdk1.8會(huì)在 鏈表長(zhǎng)度超過閾值的時(shí)候?qū)㈡湵磙D(zhuǎn)換為紅黑樹的原因!(鏈表時(shí)間復(fù)雜度為O(n),紅黑樹為 O(logn).
總結(jié)
相信到了現(xiàn)在,HashMap的各類問題各位應(yīng)該都能夠明白了,我們通過閱讀源碼的方式較為詳細(xì)的分析了 HashMap(jdk1.8)中的關(guān)鍵方法(put,get,resize),明白了HashMap中的每一個(gè)成員變量,靜態(tài)常量的含義,
另外我們還通過源碼知道了多線程環(huán)境下HashMap會(huì)出現(xiàn)的問題,引申出了ConCurrentHashMap的解析,下一章,我們同樣將通過源碼解析ConCurrentHashMap!
關(guān)注我,這里只有干貨!