HashMap
是一個基於哈希表實現的無序的key-value
容器,它鍵和值容許設置爲 null
,同時它是線程不安全的。java
jdk 1.7
中HashMap
是以數組+鏈表的實現的jdk1.8
開始引入紅黑樹,HashMap
底層變成了數組+鏈表+紅黑樹實現紅黑樹是一種特殊的平衡二叉樹,它有以下的特徵:數組
NULL
節點)因此紅黑樹的時間複雜度爲: O(lgn)
。安全
HashMap
的底層首先是一個數組,元素存放的數組索引值就是由該元素的哈希值(key-value
中key
的哈希值)肯定的,這就可能產生一種特殊狀況——不一樣的key
哈希值相同。數據結構
在這樣的狀況下,因而引入鏈表,若是key
的哈希值相同,在數組的該索引中存放一個鏈表,這個鏈表就包含了全部key
的哈希值相同的value
值,這就解決了哈希衝突的問題。多線程
可是若是發生大量哈希值相同的特殊狀況,致使鏈表很長,就會嚴重影響HashMap
的性能,由於鏈表的查詢效率須要遍歷全部節點。因而在jdk1.8
引入了紅黑樹,當鏈表的長度大於8,且HashMap
的容量大於64的時候,就會將鏈表轉化爲紅黑樹。ide
// jdk1.8 // HashMap#putVal // binCount 是該鏈表的長度計數器,當鏈表長度大於等於8時,執行樹化方法 // TREEIFY_THRESHOLD = 8 if (binCount >= TREEIFY_THRESHOLD - 1) treeifyBin(tab, hash); // HashMap#treeifyBin final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // MIN_TREEIFY_CAPACITY=64 // 若 HashMap 的大小小於64,僅擴容,不樹化 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); } }
所謂的加載因子,也叫擴容因子或者負載因子,它是用來進行擴容判斷的。函數
假設加載因子是0.5,HashMap
初始化容量是16,當HashMap
中有16 * 0.5=8
個元素時,HashMap
就會進行擴容操做。性能
而HashMap
中加載因子爲0.75,是考慮到了性能和容量的平衡。this
由加載因子的定義,能夠知道它的取值範圍是(0, 1]。spa
HashMap
的容量(size
屬性,構造函數中的initialCapacity
變量)有一個要求:它必定是2的冪。因此加載因子選擇了0.75就能夠保證它與容量的乘積爲整數。// 構造函數 public HashMap(int initialCapacity, float loadFactor) { // …… this.loadFactor = loadFactor;// 加載因子 this.threshold = tableSizeFor(initialCapacity); } /** * Returns a power of two size for the given target capacity.返回2的冪 * MAXIMUM_CAPACITY = 1 << 30 */ 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
的默認初始容量是16,而每次擴容是擴容爲原來的2倍。這裏的16和2倍就保證了HashMap
的容量是2的n次冪,那麼這樣設計的緣由是什麼呢?
與運算&
,基於二進制數值,同時爲1結果爲1,不然就是0。如1&1=1,1&0=0,0&0=0。使用與運算的緣由就是對於計算機來講,與運算十分高效。
在給HashMap
添加元素的putVal
函數中,有這樣一段代碼:
// n爲容量,hash爲該元素的hash值 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);
它會在添加元素時,經過i = (n - 1) & hash
計算該元素在HashMap
中的位置。
當 HashMap 的容量爲 2 的 n 次冪時,他的二進制值是100000……(n個0),因此n-1的值就是011111……(n個1),這樣的話(n - 1) & hash
的值纔可以充分散列。
舉個例子,假設容量爲16,如今有哈希值爲1111,1110,1011,1001四種將被添加,它們與n-1(15的二進制=01111)的哈希值分別爲11十一、11十、11十、1011,都不相同。
而假設容量不爲2的n次冪,假設爲10,那麼它與上述四個哈希值進行與運算的結果分別是:010一、0100、000一、0001。
能夠看到後兩個值發生了碰撞,從中能夠看出,非2的n次冪會加大哈希碰撞的機率。因此 HashMap 的容量設置爲2的n次冪有利於元素的充分散列。
參考:HashMap初始容量爲何是2的n次冪及擴容爲何是2倍的形式
HashMap
會致使死循環是在jdk1.7
中,因爲擴容時的操做是使用頭插法,在多線程的環境下可能產生循環鏈表,由此致使了死循環。在jdk1.8
中改成使用尾插法,避免了該死循環的狀況。
在網上找到了比較詳細的解釋分析博客與視頻: