public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
常量
// 默認初始化容量 16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默認加載因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 鏈 轉 tree 的 節點個數 下限閾值 static final int TREEIFY_THRESHOLD = 8; // tree 轉 鏈 的 節點個數 上限閾值 static final int UNTREEIFY_THRESHOLD = 6; // 鏈 轉 tree 時 優先存儲數組table容量下限閾值. table.length小於此值時則只作resize()擴容。 static final int MIN_TREEIFY_CAPACITY = 64;
變量
// Node存儲數組,在resize方法中初始化或擴容. 長度必定是 2的次方! transient Node<K,V>[] table; // 內部類 EntrySet,值對緩存 transient Set<Map.Entry<K,V>> entrySet; // table中Node的數量 transient int size; // 結構更改的次數。與AbstractList相似 (See ConcurrentModificationException) transient int modCount; // 下次需擴容size閾值: capacity * loadFactor, 或 外部指定initCap時tableSizeFor方法計算出的初始容量 int threshold; // 加載因子 用於肯定threshold final float loadFactor;
loadFactorjava
加載因子表示hash表中元素的填滿的程度, 默認是0.75。因子越大,填滿的元素越多, 好處是:空間利用率高了, 但衝突的機會加大了;因子越小,則反之。
衝突的機會越大帶來查找成本越大,因此須要在兩者間尋求平衡。node
threshold數組
構造函數指定initialCapacity時,經過tableSizeFor方法計算出的初始容量值(2的次方);其餘時候表示下次需擴容時變量size的閾值threshold = capacity * loadFactor緩存
構造函數
只是肯定好幾個成員變量的初值,並不實例化table。真正的實例化是在resize方法中!函數
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; this.threshold = tableSizeFor(initialCapacity); }
這裏有個重要的方法 tableSizeFor,保證table的初始容量是2的次方性能
外部指定initialCapacity時,該方法返回 >= initialCapacity 最接近的 2的次方.優化
n |= n >>> 1 : 先計算>>>, 按位或後賦值。 等價於 n = n | (n >>> 1)this
// 返回 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; }
Node
鏈表結構下的值對存儲對象,保存key在hash方法獲得的hash值,並連接下一個Nodespa
static class Node<K,V> implements Map.Entry<K,V> {final int hash;final K key;V value;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;} ...
hashMap無序 & LinkedHashMap 有序
- LinkedHashMap.Entry 超類是 HashMap.Node ,但增長先後指向,維護雙向鏈表的結構。
- LinkedHashMap 有成員變量(head、tail)來指向鏈表的首尾端。
HashMap的訪問(values()、keySet()、entrySet())遍歷是table[] 數組; 而LinkedHashMap的訪問遍歷是其維護的雙向鏈表。線程
eg.
TreeNode
紅黑樹 結構下的存儲對象,繼承自LinkedHashMap.Entry 依然能夠維護雙向鏈表的結構。超類依然是 HashMap.Node
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); } ... }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); }}
樹化
鏈表操做O(n)隨N的增加而性能愈差,jdk8將達到閾值的鏈轉換爲樹。
putVal
調用入口 put 、merge、compute 等方法。
在鏈表末尾新增節點後,斷定: 鏈長度 > TREEIFY_THRESHOLD(8) 時,執行treeifyBin方法:
putVal -> treeifyBin
斷定 table.length < MIN_TREEIFY_CAPACITY (64) :
true 則 resize()擴容 ; false 則 轉換爲樹結構 -> treeify方法。
resize
實例化table[]
-
putVal 方法斷定table爲空 則 執行resize()來初始實例化
-
putVal 方法執行結束前,斷定 size > threshold 執行 resize()擴容
鏈化
resize -> split
斷定table[index]是TreeNode,執行split方法來處理重定位後TreeNode是否須要樹轉鏈。
區分好移動與否的節點集合後:因必定是 由 index 移動到 index+ oldCap,因此直接斷定各自集合內節點數量 <= UNTREEIFY_THRESHOLD (6): 轉爲鏈結構 -> untreeify方法。
重定位
put操做對於鏈表是 後插入。
在低版本中,resize()重定位操做移動到同一新index下的Node鏈是 前插入。並發下,原鏈 A->B->nil 對於錯誤線程可能演變爲循環鏈 A->B->A。
在JDK8中優化了重定位方法來保證移動後節點在鏈表中的相對前後順序不變。
(node.hash & oldCap) == 0 則index不變;不然在新table上移動:newIndex = oldIndex + oldCap。
推演resize()
- 若須要移動,必定是由 index 移到 index + oldCap; 換而言之:table[index+ oldCap] 上的節點必定是由index移動而來。
- 小於等於(oldCap-1)的數不會移動。 由於在oldCap最高位(含)向左 都是 0 。
前提:
-
table.length 必定是 2 的次方。
(默認是 1 << 4 ; 指定initCap則通過 tableSizeFor處理,保證是2的次方) -
table擴容大小翻倍: newCap = oldCap << 1 左移1位
-
定位:index = node.hash & ( cap -1 )
oldCap = 16 newCap = oldCap << 1 = 32舊下標位置: e.hash & (oldCap-1) :eg1:hash 二進制值 e.hash = 10 0000 1010 oldCap-1 = 15 0000 1111 & = 10 0000 1010 eg2:hash 二進制值 e.hash = 17 0001 0001 oldCap-1 = 15 0000 1111 & = 1 0000 0001比較斷定Node在新table的位置是否須要移動: e.hash & oldCap eg1:hash 二進制值 e.hash = 10 0000 1010 oldCap = 16 0001 0000 & = 0 0000 0000 爲0 eg2:hash 二進制值 e.hash = 17 0001 0001 oldCap = 16 0001 0000 & = 1 0001 0000 不爲0 新下標位置: e.hash & (newCap-1)eg1: hash 二進制值 e.hash = 10 0000 1010 newCap-1 = 31 0001 1111 & = 10 0000 1010結論:下標不變eg1: hash 二進制值 e.hash = 17 0001 0001 newCap-1 = 31 0001 1111 & = 17 0001 0001 oldIndex + oldCap = 1 + 16結論:元素位置在擴容後數組中的位置發生了改變,新的下標位置是原下標位置+原數組長度
在上例中:
oldCap = 16 0001 0000newCap = 32 0010 0000 oldCap左移1位,末尾補0(oldCap - 1) = 15 0000 1111(newCap - 1) = 31 0001 1111 (oldCap - 1) 左移1位,末尾補1
(newCap - 1) 與 (oldCap - 1) 兩者差異在最高位:(oldCap - 1)是 0 , (newCap -1) 是 1 。
因此:
hash & ( oldCap - 1) 與 hash & (newCap -1) 不一樣之處在最高位;餘下位的值是相同的,正好對應oldIndex值 ;
加之:
(newCap -1) 與 oldCap 的 相同之處是 最高位都是1 。且 oldCap 除高位外餘下位數固定是 0;
因此 :
(hash & oldCap)運算後只會在oldCap的最高位上結果不一樣,其他位(即便hash位數大於oldCap位數) "因oldCap除高位外餘下位數都是0 " 而爲0。
由此推出:newIndex = oldIndex + 最高位&運算結果值 !
oldCap 的最高位是 1 ,因此取決於hash值在oldCap最高位上的數值
最高位是 0 則 不變 -> newIndex = oldIndex;
最高位是 1 則 移動 -> newIndex = oldIndex + oldCap 。
(非2的次方, 除最高位後餘下位不必定是0。因此 (hash & oldCap)運算後,不只最高位的結果會不一樣,餘下位的結果也可能不一樣。沒法推出結論等式,不成立)
Q&A
HashMap: 無限制
HashTable: Key與Value都不能爲null
TreeMap: Key不能爲null (經過Key來排序,因此Key要繼承java.util.Comparator)
ConcurrentHashMap: Key與Value都不能爲null
Q: 爲何鏈表與紅黑樹互轉的閾值是六、8 ?
A:若是選擇6和8(若是鏈表小於等於6樹還原轉爲鏈表,大於等於8轉爲樹),中間有個差值7能夠有效防止鏈表和樹頻繁轉換。假設一下,若是設計成鏈表個數超過8則鏈表轉換成樹結構,鏈表個數小於8則樹結構轉換成鏈表,若是一個HashMap不停的插入、刪除元素,鏈表個數在8左右徘徊,就會頻繁的發生樹轉鏈表、鏈表轉樹,效率會很低。Q: 爲何加載因子loadFactor默認是 0.75 ?
A:理想狀況下,在隨機哈希碼下容器中的節點遵循泊松分佈
Q: HashMap 中的 key若 Object類型, 則需實現哪些方法 ?
A:這裏也間接代表了: hashCode() 相同,equals() 不必定相同(所謂的Hash碰撞); 而 equals() 相同, hashCode() 必定相同。
Q: 爲何 HashMap 中 String、Integer 這樣的包裝類適合做爲 key 鍵 ?
A:![]()