所使用的jdk版本爲1.8.0_172版本,先看一下 HashMap<K,V> 在JDK中Map的UML類圖中的主要繼承實現關係:
概述
在JDK 1.7 中,HashMap的底層數據結構使用的是 Entry數組 + Entry鏈表,如果HashMap中的key值數hashCode都一樣(極端hash碰撞情況), 那所有數據就會一直都落在同一條Entry鏈表上, 此時設計初衷爲實現快速查找的HashMap就退化爲鏈表, 操作的時間複雜度成爲O(n)。JDK 8 中採用 Node數組 + Node鏈表 + TreeNode 紅黑樹,優化了極端hash碰撞情況下的查詢效率,最壞時間複雜度爲O(log n)。
HashMap 的實現不是同步的,即線程不安全的。HashMap 允許使用 null 作爲鍵和值。
數據結構
Node<K,V>
static class Node<K,V> implements Map.Entry<K,V> {
//hash值,hash(key)方法計算得出(key.hashCode和擾動函數得出)
final int hash;
//鍵
final K key;
//值
V value;
//指向下個Node節點的引用
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
TreeNode<K,V>
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
/**
* 紅黑樹節點
* 紅黑樹性質:
* 1.節點是紅色或黑色。
* 2.根節點是黑色。
* 3.每個葉子節點都是黑色的空節點(NIL節點)。
* 4 每個紅色節點的兩個子結點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
* 5.從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
//.......
注意到 TreeNode<K,V> 繼承了LinkedHashMap 中的 Entry<K,V>,而該 Entry<K,V> 類又繼承了 HashMap 中的Node<K,V>(即上面的 Node<K,V>類):
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
Node<K,V>[]
/**
* 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.)
*/
/**
* Node[]數組table在首次使用時初始化,並且根據需要調節大小。
* 分配大小後,長度總是2的冪。
* 長度總是2的冪好處:(n - 1) & hash 計算key將被放置的槽位,n 爲 table 長度;比 %取模運算效率高
* (在某些操作中,我們還允許長度爲零,以允許使用當前不需要的引導機制。)
*/
transient Node<K,V>[] table;
構造方法
HashMap 中提供了四種構造方法:
//默認構造方法,使用默認的加載因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 構造一個帶指定初始容量和加載因子的空 HashMap
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//根據初始容量計算數組下一次觸發resize的閾值
this.threshold = tableSizeFor(initialCapacity);
}
//構造一個映射關係與指定 Map 相同的新 HashMap
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
HashMap觸發數組下一次 resize 擴容操作的閾值 threshold = capacity(容量) * load factor(加載因子),HashMap中的映射元素總數超過threshold時,就會觸發 resize 擴容操作。
tableSizeFor方法
/**
* Returns a power of two size for the given target capacity.
*/
/**
* 返回一個比給定整數大且最接近的2的冪次方正整數
* 原理:通過不斷把第一個1開始後面的位變成1,再返回 n + 1,即爲2的冪
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
put(K key, V value)方法
/**
* 在此映射中關聯指定值與指定鍵。如果該映射以前包含了一個該鍵的映射關係,則舊值被替換。
*
* @param key 鍵
* @param value 值
* @return 與 key 關聯的舊值;如果 key 沒有任何映射關係,則返回 null。(返回 null 還可能表示該映射之前將 null 與 key 關聯。)
*/
public V put(K key, V value) {
//hash(key):調用擾動函數計算hash值
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//通過resize()方法初始化table
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(n - 1) & hash 相當於 hash % n;如果數組中該位置爲null,即沒有被使用,直接構造新的Node節點放入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {//如果數組中的該位置已經被佔用
Node<K,V> e; K k;
//如果鏈表的第一個節點或者樹的根節點的key與待放入的key相同,
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//用一個臨時節點e記錄
//如果數組table中的位置不是該鍵,並且數組中的節點是紅黑樹節點,則按紅黑樹節點處理
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果數組table中的位置不是該鍵,並且數組中的節點是鏈表結構節點
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//判斷鏈表的長度是否達到了 8 個
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
/*如果數組的長度沒有超過64,則進行擴容操作;
*如果數組長度超過64,則把衝突的鏈表結構轉換爲紅黑樹結構*/
treeifyBin(tab, hash);
break;
}
//如果鏈表中已經存在了待插入的key,跳出循環
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//key已經存在的情況,用新值替換舊值,返回舊值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//回調方法,提供給 LinkedHashMap 後處理的回調
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//如果HashMap中映射的數量達到了閾值,則觸發擴容操作
//如果數據量很大,擴容的時間開銷不可忽略,要考慮到。
if (++size > threshold)
resize();
afterNodeInsertion(evict);//提供給LinkedHashMap使用的回調,這裏爲空實現
return null;
}
treeifyBin方法
當鏈表長度大於 8 的時候,會進到terrifyBin()方法中:
/**
* 前提:鏈表的長度超過了 8
* 如果數組的長度沒有超過64,則進行擴容操作;
* 如果數組長度超過64,則把衝突的鏈表結構轉換爲紅黑樹結構
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
擴容方法 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) {
//擴容前數組長度超過最大值,2^30;此時數組已經很大,不再擴容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果舊數組長度超過了默認容量16,並且舊數組長度的二倍不超過 2^30
//則把新數組長度設爲舊數組長度二倍
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
//默認構造方法創建HashMap的情況
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
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//擴容後,對新擴容後的table賦值,重新計算元素新的位置
//擴容時數組長度由2的n次冪變爲2的n+1次冪, 而hash根據2的n+1次冪取模的值即是hash轉爲二進制後的後n位,
// 所以如果hash的第n位(可用hash & 舊數組長度計算)爲0的話數據在擴容後新數組中的位置就和在舊數組中的位置相同, 爲1的話數據在擴容後新數組中的位置就是(原位置+舊數組長度位置).
// 擴容時舊數組在 j 位置上的數據會根據hash的第n位是0還是1拆分爲2組, 然後把爲0的放到新數組的j位置, 爲1的放到 j+舊數組長度位置
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//判斷當前遍歷下的該node是否爲空,將j位置上的節點保存到e, 然後將oldTab[j]置爲空。
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//舊數組上的普通節點(沒有後綴節點),根據e.hash & (newCap - 1)計算出它在新數組上的位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//舊數組上節點已經是紅黑樹節點的情況:
//把舊數組的紅黑樹節點數據按照上面的邏輯計算位置(得到兩個紅黑樹)重新樹化,
//如果新樹的節點數量 <= UNTREEIFY_THRESHOLD (默認爲 6),則把樹結構降爲鏈表
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//舊數組節點爲鏈表節點的情況:
//上面所述,用e.hash & oldCap得到hash值的第n位,拆成爲0和1的兩個鏈表,對應放到原位置j和新位置(j + oldCap)
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;
}