1. 程式人生 > >細說java系列之HashMap原理

細說java系列之HashMap原理

hashmap 屬於 情況 int 數據結構 boolean 條件 com ext

技術分享圖片

類圖

在正式分析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原理