在本篇主要整理一下 1.8 的 HashMap 進行分析,主要從以下方面:
- 存儲結構
- 擴容機制
基本屬性
下面列出 HashMap 中的屬性值并加以節是
// 部分常量 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始大小 16 static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 負載因子,當 size 超過負載因子與目前數量的乘積時會再添加節點會進行擴容 static final int TREEIFY_THRESHOLD = 8; // 連結清單大小大于該值時轉為紅黑樹 // 屬性 transient Node<K,V>[] table; // HashMap 的基本結構:數組 transient int size; // 目前數量 int threshold; // 目前容量門檻值(size * threshold) final float loadFactor; // 負載因子,final 修飾,确定後不可修改
構造函數
// 可以對 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; // 下面的 tableSizeFor 主要将容量調整為 2 的幂 this.threshold = tableSizeFor(initialCapacity); }
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; }
在了解 HashMap 的整體結構前,先來看看其節點構成
Node
Node 繼承自 Map.Entry,是一種鍵值存儲結構。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; //若 hash 值相同,則會對比 key 确定是否為同一對象 final K key; // 鍵值 V value; // 值 Node<K,V> next; // 下一指針,用于構造連結清單解決 hash 沖突問題 // ... }
hash 操作
取 key.hashCode() 進行 hash 操作,是以重寫
equals
方法時,要對
hashcode
進行重寫,不然可能導緻
equals
相同,
hashcode
不同的結果(hashcode 預設是對象存儲的記憶體位址,對具有相同屬性值的對象也會判定為不相等)。
static final int hash(Object key) { int h; // >>> 無符号右移: // 0000 0100 1011 0011 1101 1111 1110 0001 >>>(16) 0000 0000 0000 0000 0000 0100 1011 0011 // 令低位摻雜高位特征 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
get 操作
擷取操作時
(n - 1) & hash
替代對數組長度的取餘操作,提高計算速率。
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 檢視對應位置的第一個節點,同時需要使用 equals 比較 if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; // 進行 連結清單 或 紅黑樹 的查找操作 if ((e = first.next) != null) { 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); } } return null; }
put 操作
// 傳入時對 key 進行 hash,放入對應的位置 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) // resize 對 hashmap 數組進行初始化或者擴容,此時為擴容 n = (tab = resize()).length; // 要放置的數組的對應位置沒有元素,則直接插入即可 // (n - 1) & hash 防止長度溢出,使用 & 運算取代 % 運算,提高效率 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // key 相同則替換值 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 若該 node 為 TreeNode,對其進行樹相關操作 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; }
resize
用于初始化或擴容,每次擴容都為 2 的幂,擴容完成後原來的元素會保持相同的索引或者較原來有 2 的幂的量的偏移。
重新分布時,用
(e.hash & oldCap) == 0
作為區分,比起 JDK7 重新計算 hash,得到效率上的提升。
final Node<K,V>[] resize() { // 擷取舊表 table 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; } // 擴容,并調整 threshold 大小 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // threshold 翻倍 } // 帶參數的初始化 else if (oldThr > 0) // 初始容量被設為 threshold newCap = oldThr; // 不帶參數的初始化 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 計算新的 threshold 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) // split 方法對樹進行拆分,若拆分後節點數量太少會取消樹化 ((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; // 通過該方式進行重新散列 // 使用 (e.hash & oldCap) 區分索引位置 // 等于0則為原位置,否則位置為目前偏移舊數組長度 // lo:記錄索引不變的節點 // hi:記錄索引偏移的節點 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; }