細說java系列之HashMap原理
類圖
在正式分析HashMap實現原理之前,先來看看其類圖。
源碼解讀
下面集合HashMap的put(K key, V value)方法探究其實現原理。
// 在HashMap內部用於存放插入數據的是一個名為"table"的一維Node對象數組
// Node對象為實際存放插入數據Key和Value的數據結構
transient Node<K,V>[] table;
// 外部調用插入數據的接口方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 內部真正執行數據插入的方法
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)
// 第一次插入數據時,初始化table數組
n = (tab = resize()).length;
if ((p = tab[i = (n - 1 ) & hash]) == null)
// 變量n為HashMap當前容量大小,實際上就是table數組的容量大小
// 將(n-1)與插入數據Key的hashcode值進行邏輯與運算,找到一個隨機位置i
// 如果table[i]值為null,說明該位置還沒有存放數據,新建一個Node對象並存放在table[i],本次插入完畢,返回null值
tab[i] = newNode(hash, key, value, null);
else {
// 如果table[i]值不為null,說明該位置已經存放了數據,繼續尋找插入數據的位置
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果新插入數據Key的hashcode值與table[i]位置存放對象Key的hascode值相同
// 並且新插入數據Key與table[i]位置存放對象的Key引用的是同一個對象或者它們相等(通過equals方法比較)
// 則使用新插入數據的Value替換table[i]位置存放對象的Value,本次插入完畢,返回之前存放在該位置對象的Value值
e = p;
else if (p instanceof TreeNode)
// 如果table[i]位置存放對象屬於TreeNode類型,進行特別處理
// 為什麽需要判斷是否為TreeNode類型?
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 如果新插入數據Key不與table[i]位置存放對象Key相同,那麽尋找一個滿足如下條件的位置,將新數據插入到對應位置
// 條件1:如果table[i]位置對象的next屬性為null,直接通過該next屬性引用插入數據新建的Node對象,並返回null
// 條件2:如果table[i]位置對象的next屬性不為null,那麽就在該位置對象鏈表上尋找一個插入新數據的位置,在這個過程中根據如下滿足條件進行處理
// 條件3:如果插入數據的Key與鏈表上的某個Node對象的Key相同,那麽使用新插入的Value替換該Node對象的Value,並返回該Node之前的Value值
// 如果不滿足上訴3個條件,將插入數據保存在table[i]位置對象鏈表的末端,並返回null
// 總結:HashMap存放實際數據的是一個一維數組,而每一個數組元素又支持鏈表結構
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;
}
將上述HashMap實現插入數據的過程以插入4個數據為示例描述如下:
1.插入第一個數據時,初始化HashMap內部名為“table”的一維數組,默認大小為16,每一個數組元素值為null。
尋找一個插入數據的位置i,這在HashMap中的實現非常巧妙,這個插入位置通過如下表達式計算得到:i = (n - 1) & hash
。其中,n為當前HashMap的容量,其實就是內部table數組的大小,hash為插入數據Key的hashCode值。通過該表達式將會隨機找個一個插入位置i,i的值範圍為[0,n-1]。必須註意的是: 插入位置是隨機的!並不是按照一維數組的順序插入方式,這是因為HashMap這個數據結構的特點所決定的。因為是插入第一個數據,所以隨機找到的位置“i=3”處對象為null值,因此直接在該位置處插入一個Node對象。本次插入操作完畢,返回null值。
2.插入第二個數據時,先隨機找到一個插入位置“i=1”,而且該位置處的對象為null值,說明還沒有存放任何數據,直接在該位置處插入一個Node對象。本次插入操作完畢,返回null值。
3.插入第三個數據時,隨機找到插入位置“i=1”,該位置上已經存放了數據;並且插入數據的Key不與該位置Node對象的Key相同(Key相同的條件時:首先必須hashCode值相同,並且他們引用的是同一個對象或者他們通過equals()方法比較時相等),此時需要將新插入數據保存到該位置Node對象的next屬性中(看起來像是鏈接到該位置Node對象的尾部)。本次插入操作完畢,返回null值。
4.插入第四個數據時,隨機找到插入位置“i=1”,該位置上已經存放了數據;並且插入數據的Key與該位置Node對象的Key相同,此時使用新插入數據的Value替換該位置Node對象當前的Value值。本次插入操作完畢,返回該位置Node對象之前的Value值。
上述示例描述的就是HashMap插入數據的原理,實際上除了上述描述的核心操作之外,在返回值之前需要判斷HashMap當前的容量是否能夠存儲更多插入的數據,根據判斷之後可能會進行擴容,如下代碼所示:
if (++size > threshold)
resize();
總結
1.先明確一個事實,HashMap內部實際存放數據的是一個一維數組,但是存儲的元素類型支持鏈表結構。所以,存放數據之後的HashMap看起來像是一個“二維數組”(註意: 並不是真正的二維數組)。
2.判斷HashMap存放對象Key是否相同,方法如下:
- 新插入Key的hashCode值必須與已經存在對象Key的hashCode值相等,這是前提
- 新插入Key與已存在對象Key引用的是同一個對象,或者他們通過equals()方法比較時相等
3.HashMap內部名為“table”的一維數組可能存在“存不滿”數據的情況,因為插入數據的位置是通過表達式i = (n - 1) & hash
計算的,可以認為這是一個隨機的值。
4.最後,還是需要老生常談地強調一下,HashMap不是線程安全的,其內部用於存放數據的容器本質上是一個一維數組,該數組本身並不是線程安全的,而且HashMap在寫操作時也並未進行線程同步。如果需要使用線程安全的HashMap,應該使用ConcurrentHashMap,因為在其中用於存儲數據的數組是線程安全的:
/**
* The array of bins. Lazily initialized upon first insertion.
* Size is always a power of two. Accessed directly by iterators.
*/
// ConcurrentHashMap內部存儲數據的table通過關鍵字volatile修飾,因此是線程安全的
transient volatile Node<K,V>[] table;
細說java系列之HashMap原理