概述
HashMap在底層數據結構上采用了數組+鏈表+紅黑樹,通過散列映射來存儲鍵值對數據因為在查詢上使用散列碼(通過鍵生成一個數字作為數組下標,這個數字就是hash code)所以在查詢上的訪問速度比較快,HashMap最多允許一對鍵值對的Key為Null,允許多對鍵值對的value為Null。它是非線程安全的。在排序上面是無序的。
HashMap的初始容量為16,填充因子默認是0.75。
HashMap擴容時是當前容量翻倍即:capacity*2,capacity為當前容量。
HashMap的擴容操作是一項很耗時的任務,所以如果能估算Map的容量,最好給它一個默認初始值,避免進行多次擴容。HashMap的線程是不安全的,多線程環境中推薦是ConcurrentHashMap。
HashMap的實現原理
簡單說下HashMap的實現原理:
首先有一個每個元素都是鏈表(可能表述不准確)的數組,當添加一個元素(key-value)時,就首先計算元素key的hash值,以此確定插入數組中的位置,但是可能存在同一hash值的元素已經被放在數組同一位置了,這時就添加到同一hash值的元素的后面,他們在數組的同一位置,但是形成了鏈表,同一各鏈表上的Hash值是相同的,所以說數組存放的是鏈表。而當鏈表長度太長時,鏈表就轉換為紅黑樹,這樣大大提高了查找的效率。
當鏈表數組的容量超過初始容量的0.75時,再散列將鏈表數組擴大2倍,把原鏈表數組的搬移到新的數組中
即HashMap的原理圖是:
一,JDK1.8中的涉及到的數據結構
1,位桶數組
- transient Node<k,v>[] table;//存儲(位桶)的數組</k,v>
2,數組元素Node<K,V>實現了Entry接口
1 //Node是單向鏈表,它實現了Map.Entry接口 2 static class Node<k,v> implements Map.Entry<k,v> { 3 final int hash; 4 final K key; 5 V value; 6 Node<k,v> next; 7 //構造函數Hash值 鍵 值 下一個節點 8 Node(int hash, K key, V value, Node<k,v> next) { 9 this.hash = hash; 10 this.key = key; 11 this.value = value; 12 this.next = next; 13 } 14 15 public final K getKey() { return key; } 16 public final V getValue() { return value; } 17 public final String toString() { return key + = + value; } 18 19 public final int hashCode() { 20 return Objects.hashCode(key) ^ Objects.hashCode(value); 21 } 22 23 public final V setValue(V newValue) { 24 V oldValue = value; 25 value = newValue; 26 return oldValue; 27 } 28 //判斷兩個node是否相等,若key和value都相等,返回true。可以與自身比較為true 29 public final boolean equals(Object o) { 30 if (o == this) 31 return true; 32 if (o instanceof Map.Entry) { 33 Map.Entry<!--?,?--> e = (Map.Entry<!--?,?-->)o; 34 if (Objects.equals(key, e.getKey()) && 35 Objects.equals(value, e.getValue())) 36 return true; 37 } 38 return false; 39 }
3,紅黑樹
1 //紅黑樹 2 static final class TreeNode<k,v> extends LinkedHashMap.Entry<k,v> { 3 TreeNode<k,v> parent; // 父節點 4 TreeNode<k,v> left; //左子樹 5 TreeNode<k,v> right;//右子樹 6 TreeNode<k,v> prev; // needed to unlink next upon deletion 7 boolean red; //顏色屬性 8 TreeNode(int hash, K key, V val, Node<k,v> next) { 9 super(hash, key, val, next); 10 } 11 12 //返回當前節點的根節點 13 final TreeNode<k,v> root() { 14 for (TreeNode<k,v> r = this, p;;) { 15 if ((p = r.parent) == null) 16 return r; 17 r = p; 18 } 19 }
二,源碼中的數據域
加載因子(默認0.75):為什么需要使用加載因子,為什么需要擴容呢?因為如果填充比很大,說明利用的空間很多,如果一直不進行擴容的話,鏈表就會越來越長,這樣查找的效率很低,因為鏈表的長度很大(當然最新版本使用了紅黑樹后會改進很多),擴容之后,將原來鏈表數組的每一個鏈表分成奇偶兩個子鏈表分別掛在新鏈表數組的散列位置,這樣就減少了每個鏈表的長度,增加查找效率
HashMap本來是以空間換時間,所以填充比沒必要太大。但是填充比太小又會導致空間浪費。如果關注內存,填充比可以稍大,如果主要關注查找性能,填充比可以稍小。
1 public class HashMap<k,v> extends AbstractMap<k,v> implements Map<k,v>, Cloneable, Serializable { 2 private static final long serialVersionUID = 362498820763181265L; 3 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 4 static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量 5 static final float DEFAULT_LOAD_FACTOR = 0.75f;//填充比 6 //當add一個元素到某個位桶,其鏈表長度達到8時將鏈表轉換為紅黑樹 7 static final int TREEIFY_THRESHOLD = 8; 8 static final int UNTREEIFY_THRESHOLD = 6; 9 static final int MIN_TREEIFY_CAPACITY = 64; 10 transient Node<k,v>[] table;//存儲元素的數組 11 transient Set<map.entry<k,v>> entrySet; 12 transient int size;//存放元素的個數 13 transient int modCount;//被修改的次數fast-fail機制 14 int threshold;//臨界值 當實際大小(容量*填充比)超過臨界值時,會進行擴容 15 final float loadFactor;//填充比(......后面略)
三,HashMap的構造函數
HashMap的構造方法有4種,主要涉及到的參數有,指定初始容量,指定填充比和用來初始化的Map
1 //構造函數1 2 public HashMap(int initialCapacity, float loadFactor) { 3 //指定的初始容量非負 4 if (initialCapacity < 0) 5 throw new IllegalArgumentException(Illegal initial capacity: + 6 initialCapacity); 7 //如果指定的初始容量大於最大容量,置為最大容量 8 if (initialCapacity > MAXIMUM_CAPACITY) 9 initialCapacity = MAXIMUM_CAPACITY; 10 //填充比為正 11 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 12 throw new IllegalArgumentException(Illegal load factor: + 13 loadFactor); 14 this.loadFactor = loadFactor; 15 this.threshold = tableSizeFor(initialCapacity);//新的擴容臨界值 16 } 17 18 //構造函數2 19 public HashMap(int initialCapacity) { 20 this(initialCapacity, DEFAULT_LOAD_FACTOR); 21 } 22 23 //構造函數3 24 public HashMap() { 25 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 26 } 27 28 //構造函數4用m的元素初始化散列映射 29 public HashMap(Map<!--? extends K, ? extends V--> m) { 30 this.loadFactor = DEFAULT_LOAD_FACTOR; 31 putMapEntries(m, false); 32 }
四,HashMap的存取機制
1,HashMap如何getValue值,看源碼
1 public V get(Object key) { 2 Node<K,V> e; 3 return (e = getNode(hash(key), key)) == null ? null : e.value; 4 } 5 /** 6 * Implements Map.get and related methods 7 * 8 * @param hash hash for key 9 * @param key the key 10 * @return the node, or null if none 11 */ 12 final Node<K,V> getNode(int hash, Object key) { 13 Node<K,V>[] tab;//Entry對象數組 14 Node<K,V> first,e; //在tab數組中經過散列的第一個位置 15 int n; 16 K k; 17 /*找到插入的第一個Node,方法是hash值和n-1相與,tab[(n - 1) & hash]*/ 18 //也就是說在一條鏈上的hash值相同的 19 if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) { 20 /*檢查第一個Node是不是要找的Node*/ 21 if (first.hash == hash && // always check first node 22 ((k = first.key) == key || (key != null && key.equals(k))))//判斷條件是hash值要相同,key值要相同 23 return first; 24 /*檢查first后面的node*/ 25 if ((e = first.next) != null) { 26 if (first instanceof TreeNode) 27 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 28 /*遍歷后面的鏈表,找到key值和hash值都相同的Node*/ 29 do { 30 if (e.hash == hash && 31 ((k = e.key) == key || (key != null && key.equals(k)))) 32 return e; 33 } while ((e = e.next) != null); 34 } 35 } 36 return null; 37 }
get(key)方法時獲取key的hash值,計算hash&(n-1)得到在鏈表數組中的位置first=tab[hash&(n-1)],先判斷first的key是否與參數key相等,不等就遍歷后面的鏈表找到相同的key值返回對應的Value值即可
2,HashMap如何put(key,value);看源碼
1 public V put(K key, V value) { 2 return putVal(hash(key), key, value, false, true); 3 } 4 /** 5 * Implements Map.put and related methods 6 * 7 * @param hash hash for key 8 * @param key the key 9 * @param value the value to put 10 * @param onlyIfAbsent if true, don't change existing value 11 * @param evict if false, the table is in creation mode. 12 * @return previous value, or null if none 13 */ 14 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 15 boolean evict) { 16 Node<K,V>[] tab; 17 Node<K,V> p; 18 int n, i; 19 if ((tab = table) == null || (n = tab.length) == 0) 20 n = (tab = resize()).length; 21 /*如果table的在(n-1)&hash的值是空,就新建一個節點插入在該位置*/ 22 if ((p = tab[i = (n - 1) & hash]) == null) 23 tab[i] = newNode(hash, key, value, null); 24 /*表示有沖突,開始處理沖突*/ 25 else { 26 Node<K,V> e; 27 K k; 28 /*檢查第一個Node,p是不是要找的值*/ 29 if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) 30 e = p; 31 else if (p instanceof TreeNode) 32 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 33 else { 34 for (int binCount = 0; ; ++binCount) { 35 /*指針為空就掛在后面*/ 36 if ((e = p.next) == null) { 37 p.next = newNode(hash, key, value, null); 38 //如果沖突的節點數已經達到8個,看是否需要改變沖突節點的存儲結構, 39 //treeifyBin首先判斷當前hashMap的長度,如果不足64,只進行 40 //resize,擴容table,如果達到64,那么將沖突的存儲結構為紅黑樹 41 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 42 treeifyBin(tab, hash); 43 break; 44 } 45 /*如果有相同的key值就結束遍歷*/ 46 if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) 47 break; 48 p = e; 49 } 50 } 51 /*就是鏈表上有相同的key值*/ 52 if (e != null) { // existing mapping for key,就是key的Value存在 53 V oldValue = e.value; 54 if (!onlyIfAbsent || oldValue == null) 55 e.value = value; 56 afterNodeAccess(e); 57 return oldValue;//返回存在的Value值 58 } 59 } 60 ++modCount; 61 /*如果當前大小大於門限,門限原本是初始容量*0.75*/ 62 if (++size > threshold) 63 resize();//擴容兩倍 64 afterNodeInsertion(evict); 65 return null; 66 }
下面簡單說下添加鍵值對put(key,value)的過程:
1,判斷鍵值對數組tab[]是否為空或為null,否則以默認大小resize();
2,根據鍵值key計算hash值得到插入的數組索引i,如果tab[i]==null,直接新建節點添加,否則轉入3
3,判斷當前數組中處理hash沖突的方式為鏈表還是紅黑樹(check第一個節點類型即可),分別處理
五,HasMap的擴容機制resize();
構造hash表時,如果不指明初始大小,默認大小為16(即Node數組大小16),如果Node[]數組中的元素達到(填充比*Node.length)重新調整HashMap大小 變為原來2倍大小,擴容很耗時
1 /** 2 * Initializes or doubles table size. If null, allocates in 3 * accord with initial capacity target held in field threshold. 4 * Otherwise, because we are using power-of-two expansion, the 5 * elements from each bin must either stay at same index, or move 6 * with a power of two offset in the new table. 7 * 8 * @return the table 9 */ 10 final Node<K,V>[] resize() { 11 Node<K,V>[] oldTab = table; 12 int oldCap = (oldTab == null) ? 0 : oldTab.length; 13 int oldThr = threshold; 14 int newCap, newThr = 0; 15 16 /*如果舊表的長度不是空*/ 17 if (oldCap > 0) { 18 if (oldCap >= MAXIMUM_CAPACITY) { 19 threshold = Integer.MAX_VALUE; 20 return oldTab; 21 } 22 /*把新表的長度設置為舊表長度的兩倍,newCap=2*oldCap*/ 23 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 24 oldCap >= DEFAULT_INITIAL_CAPACITY) 25 /*把新表的門限設置為舊表門限的兩倍,newThr=oldThr*2*/ 26 newThr = oldThr << 1; // double threshold 27 } 28 /*如果舊表的長度的是0,就是說第一次初始化表*/ 29 else if (oldThr > 0) // initial capacity was placed in threshold 30 newCap = oldThr; 31 else { // zero initial threshold signifies using defaults 32 newCap = DEFAULT_INITIAL_CAPACITY; 33 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 34 } 35 36 37 38 if (newThr == 0) { 39 float ft = (float)newCap * loadFactor;//新表長度乘以加載因子 40 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 41 (int)ft : Integer.MAX_VALUE); 42 } 43 threshold = newThr; 44 @SuppressWarnings({"rawtypes","unchecked"}) 45 /*下面開始構造新表,初始化表中的數據*/ 46 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 47 table = newTab;//把新表賦值給table 48 if (oldTab != null) {//原表不是空要把原表中數據移動到新表中 49 /*遍歷原來的舊表*/ 50 for (int j = 0; j < oldCap; ++j) { 51 Node<K,V> e; 52 if ((e = oldTab[j]) != null) { 53 oldTab[j] = null; 54 if (e.next == null)//說明這個node沒有鏈表直接放在新表的e.hash & (newCap - 1)位置 55 newTab[e.hash & (newCap - 1)] = e; 56 else if (e instanceof TreeNode) 57 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 58 /*如果e后邊有鏈表,到這里表示e后面帶着個單鏈表,需要遍歷單鏈表,將每個結點重*/ 59 else { // preserve order保證順序 60 ////新計算在新表的位置,並進行搬運 61 Node<K,V> loHead = null, loTail = null; 62 Node<K,V> hiHead = null, hiTail = null; 63 Node<K,V> next; 64 65 do { 66 next = e.next;//記錄下一個結點 67 //新表是舊表的兩倍容量,實例上就把單鏈表拆分為兩隊, 68 //e.hash&oldCap為偶數一隊,e.hash&oldCap為奇數一對 69 if ((e.hash & oldCap) == 0) { 70 if (loTail == null) 71 loHead = e; 72 else 73 loTail.next = e; 74 loTail = e; 75 } 76 else { 77 if (hiTail == null) 78 hiHead = e; 79 else 80 hiTail.next = e; 81 hiTail = e; 82 } 83 } while ((e = next) != null); 84 85 if (loTail != null) {//lo隊不為null,放在新表原位置 86 loTail.next = null; 87 newTab[j] = loHead; 88 } 89 if (hiTail != null) {//hi隊不為null,放在新表j+oldCap位置 90 hiTail.next = null; 91 newTab[j + oldCap] = hiHead; 92 } 93 } 94 } 95 } 96 } 97 return newTab; 98 }
HashMap擴容可以分為三種情況:
第一種:使用默認構造方法初始化HashMap。從前文可以知道HashMap在一開始初始化的時候會返回一個空的table,並且thershold為0。因此第一次擴容的容量為默認值DEFAULT_INITIAL_CAPACITY也就是16。同時threshold = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12。
第二種:指定初始容量的構造方法初始化HashMap。那么從下面源碼可以看到初始容量會等於threshold,接着threshold = 當前的容量(threshold) * DEFAULT_LOAD_FACTOR。
第三種:HashMap不是第一次擴容。如果HashMap已經擴容過的話,那么每次table的容量以及threshold量為原有的兩倍。
什么情況會導致擴容?
1.鏈表轉換為紅黑樹時(鏈表節點個數達到8個可能會轉換為紅黑樹)。如果轉換時map長度小於64則直接擴容一倍,不轉化為紅黑樹。如果此時map長度大於64,則不會擴容,直接進行鏈表轉紅黑樹的操作。
2.map中總節點數大於閾值(即大於map長度的0.75倍)時會進行擴容。
如何擴容?
1.創建一個新的map,是原先map的兩倍。注意此過程是單線程創建的
2.復制舊的map到新的map中。
將原數組元素平移至新數組:
如果舊數組不為空,當我們在擴容時就需要將舊數組的數組遷移到新數組,數據遷移需要遍歷舊數組,將舊數組每一個數組下標index的數據移動到新數組中。如果遍歷數組時發現當前位置存放着Node鏈表,這個時候需要對Node結點的Hash值與新數組長度進行&運算。如果計算出來的值為0,將舊數組當前位置的Node鏈表賦值到新數組相同位置,即newTab[j] = loHead。如果計算出來的值不為0,此時將當前位置的Node鏈表賦值給新數組當前位置加上舊數組長度的位置,即newTab[j + oldCap] = hiHead。
如果想要理解這個過程,需要首先明白JDK1.8中HashMap如何計算數組下標。
int index = node.hash&(table.length-1);
&運算能夠保證計算出來的值小於等於其中任何一個值,因此計算出來的數組下標index小於等於table.length-1。
可是在數組兩倍擴容后,數組的長度發生了變化,同時我們也必須要嚴格遵守數組下標index的算法,否則在get時無法獲取到對應Node結點。因此將舊數組中的數據遷移到新數組后,需要按照新數組長度重新計算數組下標。如果還是通過&運算計算數組下標,我們就需要遍歷數組中所有的結點,並計算出結點的Hash值,並將Hash值與數組的長度減1進行&運算得出數組下標。而HashMap的實現者明顯找到了更便捷的算法。而這種算法分為兩種情況。
注:注意Node結點Hash值二進制表示的標紅位數變化。
情況一:Hash值與舊數組長度的&運算不為0
node.hash & oldTable.length != 0;
我們假設Node結點的Hash值的二進制是1000010101,舊數組長度為16,二進制即10000。此時計算出來的index為5。
Hash :1000010101
length-1 :0000001111
——————————
index : 0000000101
當我們對數組進行擴容,數組的長度變成了32,Node結點的Hash值依然為1000010101。此時計算出來的index為21。
Hash :1000010101
length-1 :0000011111
——————————
index : 0000010101
結點平移后,此時計算出來的存放結點的新數組下標為舊數組下標加上舊數組的長度,即newIndex = oldIndex+oldLength;
情況二:Hash值與舊數組長度的&運算為0
node.hash & oldTable.length == 0;
我們假設Node結點的Hash值的二進制是1000000101,舊數組長度為16,二進制即10000。此時計算出來的index為5。
Hash :1000010101
length-1 :0000001111
——————————
index : 0000000101
當我們對數組進行擴容,數組的長度變成了32,Node幾點的Hash值依然為1000000101。此時計算出來的index仍為5。
Hash :1000000101
length-1 :0000011111
——————————
index : 0000000101
結點平移后,此時計算存放結點的新數組下標與舊數組下標相等,即newIndex = oldIndex。
六,JDK1.8使用紅黑樹的改進
在Java jdk8中對HashMap的源碼進行了優化,在jdk7中,HashMap處理“碰撞”的時候,都是采用鏈表來存儲,當碰撞的結點很多時,查詢時間是O(n)。
在jdk8中,HashMap處理“碰撞”增加了紅黑樹這種數據結構,當碰撞結點較少時,采用鏈表存儲,當較大時(>8個),且數組元素個數大於64時,采用紅黑樹(特點是查詢時間是O(logn))存儲(有一個閥值控制,大於閥值(8個),將鏈表存儲轉換成紅黑樹存儲)
問題分析:
你可能還知道哈希碰撞會對hashMap的性能帶來災難性的影響。如果多個hashCode()的值落到同一個桶內的時候,這些值是存儲到一個鏈表中的。最壞的情況下,所有的key都映射到同一個桶中,這樣hashmap就退化成了一個鏈表——查找時間從O(1)到O(n)。
隨着HashMap的大小的增長,get()方法的開銷也越來越大。由於所有的記錄都在同一個桶里的超長鏈表內,平均查詢一條記錄就需要遍歷一半的鏈表。
JDK1.8HashMap的紅黑樹是這樣解決的:
如果某個桶中的記錄過大的話(當前是TREEIFY_THRESHOLD = 8),HashMap會動態的使用一個專門的treemap實現來替換掉它。這樣做的結果會更好,是O(logn),而不是糟糕的O(n)。
它是如何工作的?前面產生沖突的那些KEY對應的記錄只是簡單的追加到一個鏈表后面,這些記錄只能通過遍歷來進行查找。但是超過這個閾值后HashMap開始將列表升級成一個二叉樹,使用哈希值作為樹的分支變量,如果兩個哈希值不等,但指向同一個桶的話,較大的那個會插入到右子樹里。如果哈希值相等,HashMap希望key值最好是實現了Comparable接口的,這樣它可以按照順序來進行插入。這對HashMap的key來說並不是必須的,不過如果實現了當然最好。如果沒有實現這個接口,在出現嚴重的哈希碰撞的時候,你就並別指望能獲得性能提升了。