HashMap的定義以下:node
1 public class HashMap<K,V> extends AbstractMap<K,V> 2 implements Map<K,V>, Cloneable, Serializable {}
HashMap是一個散列表,用於存儲key-value形式的鍵值對。數組
從源碼的定義中能夠看到HashMap繼承了AbstractMap抽象類並且也實現了Map<K,V>接口,AbstractMap類自己也繼承了Map<K,V>接口,Map接口定義了一些map數據結構的基本操做, AbstractMap提供了Map接口的一些默認實現。安全
HashMap實現了Cloneable接口和Serializable接口,這兩個接口自己並無定義方法,屬於申明式接口,容許hashmap進行克隆和序列化。數據結構
另外,HashMap不是線程安全的,若是須要使用線程安全的HashMap,可使用Collections類中的synchronizedMap方法來得到線程安全的HashMap:app
Map map = Collections.synchronizedMap(new HashMap());函數
1 //hashmap的初始容量:16 2 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 3 4 //hashmap的最大容量,hashmap的容量必須是2的指數值 5 static final int MAXIMUM_CAPACITY = 1 << 30; 6 7 //默認填充因子 8 static final float DEFAULT_LOAD_FACTOR = 0.75f; 9 10 //鏈表轉換爲樹的閥值:若是一個桶中的元素個數超過 TREEIFY_THRESHOLD=8 ,就使用紅黑樹來替換鏈表,從而提升速度 11 static final int TREEIFY_THRESHOLD = 8; 12 13 //樹還原爲鏈表的閾值:擴容時桶中元素小於UNTREEIFY_THRESHOLD = 6,則把樹形的桶元素還原爲鏈表結構 14 static final int UNTREEIFY_THRESHOLD = 6; 15 16 //哈希表的最小樹形化容量:當哈希表中的容量大於這個值MIN_TREEIFY_CAPACITY = 64時,哈希表中的桶才能進行樹形化,不然桶中元素過多時只會擴容,並不會進行樹形化, 爲了不擴容和樹形化選擇的衝突,這個值不能小於4* TREEIFY_THRESHOLD = 32 17 static final int MIN_TREEIFY_CAPACITY = 64; 18 19 //hashmap用於存儲數據的Node數組,長度是2的指數值 20 transient Node<K,V>[] table; 21 22 //保存entrySet返回的結果 23 transient Set<Map.Entry<K,V>> entrySet; 24 25 //hashmap中鍵值對個數 26 transient int size; 27 28 //hashmap對象修改計數器 29 transient int modCount; 30 31 // threshold=容量*裝載因子,表明目前佔用數組長度的最大值,用於判斷是否須要擴容 32 int threshold; 33 34 //裝載因子,用來衡量hashmap裝載數據程度,默認值爲EFAULT_LOAD_FACTOR = 0.75f,裝載因子計算方法size/capacity 35 final float loadFactor;
查閱資料發如今JDK1.8以前hashmap的經過數組+鏈表的數據結構實現的,這樣在hash值大量衝突時hashmap是經過一個長長的鏈表來存儲的,JDK1.8開始,hashmap採用數組+鏈表+紅黑樹組合數據結構來實現,鏈表和紅黑樹將會按必定策略互相轉換,JDK1.8開始,hashmap的存儲結構大體以下:this
回顧一下關於紅黑樹的定義:spa
1. 每一個結點或是紅色的,或是黑色的
2. 根節點是黑色的
3. 每一個葉結點(NIL)是黑色的
4. 若是一個節點是紅色的,則它的兩個兒子都是黑色的
5. 對於每一個結點,從該結點到其子孫結點的全部路徑上包含相同數目的黑色結點.net
1 public HashMap() { 2 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 3 }
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
1 public HashMap(int initialCapacity, float loadFactor) { 2 //初始容量校驗 3 if (initialCapacity < 0) 4 throw new IllegalArgumentException("Illegal initial capacity: " + 5 initialCapacity); 6 //校驗初始容量不能超過hashmap最大容量:2的30次方 7 if (initialCapacity > MAXIMUM_CAPACITY) 8 initialCapacity = MAXIMUM_CAPACITY; //初始化爲最大容量 9 //校驗裝載因子 10 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 11 throw new IllegalArgumentException("Illegal load factor: " + 12 loadFactor); 13 this.loadFactor = loadFactor; 14 this.threshold = tableSizeFor(initialCapacity); 15 } 16 //根據根據初始化參數initialCapacity 返回大於等於該值得最小 2的指數值 做爲初始容量 17 static final int tableSizeFor(int cap) { 18 int n = cap - 1; 19 n |= n >>> 1; 20 n |= n >>> 2; 21 n |= n >>> 4; 22 n |= n >>> 8; 23 n |= n >>> 16; 24 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 25 }
這個構造函數能夠發現初始化hashmap的容量並非隨意指定多少就初始化多少,內部根據傳入的容量值作了轉換,嚴格的將hashmap的初始容量轉換成的2的指數值,好比咱們初始化一個new HashMap(25),實際初始化處來的容量是32,至關於new HashMap(32)線程
1 public HashMap(Map<? extends K, ? extends V> m) { 2 this.loadFactor = DEFAULT_LOAD_FACTOR; 3 putMapEntries(m, false); 4 }
向hashmap中添加健值對
1 public V put(K key, V value) { 2 return putVal(hash(key), key, value, false, true); 3 } 4 5 //hash:hashkey 6 //key value :鍵值對 7 //onlyIfAbsent:爲true則不修改已存在的value 8 //evict:返回被修改的value 9 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 10 boolean evict) { 11 12 Node<K,V>[] tab; 13 Node<K,V> p; 14 int n, i; 15 //若是table爲null或者空,則進行resize擴容 16 if ((tab = table) == null || (n = tab.length) == 0) 17 //執行resize擴容,內部將初始化table和threshold 18 n = (tab = resize()).length; 19 //若是對應索引處沒有Node,則新建Node並放到table裏面 20 if ((p = tab[i = (n - 1) & hash]) == null) 21 tab[i] = newNode(hash, key, value, null); //tab[i]==null的狀況,直接新建立節點並賦值給tab[i] 22 else { 23 //else的狀況表示tab[i]不爲null 24 Node<K,V> e; 25 K k; 26 //1:hash值與tab[i]的hash值相等且key也相等,那麼覆蓋該節點的value域 27 if (p.hash == hash && 28 ((k = p.key) == key || (key != null && key.equals(k)))) 29 e = p;//暫存tab[i]的節點p到臨時變量e 30 //2:判斷tab[i]是不是紅黑樹 31 else if (p instanceof TreeNode) 32 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//添加到樹形結構中 33 else { 34 //3:不是紅黑樹 且不是第1中狀況,即:hash值一致,可是key不一致,那麼須要將新的key-value添加到鏈表末尾 35 for (int binCount = 0; ; ++binCount) { 36 if ((e = p.next) == null) { 37 //添加到鏈表末尾 38 p.next = newNode(hash, key, value, null); 39 //若是該節點的鏈表長度大於8,則須要將鏈表轉換爲紅黑樹 40 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 41 treeifyBin(tab, hash); 42 break; 43 } 44 //若是key已經存在該鏈表中,直接break,執行後續更新邏輯 45 if (e.hash == hash && 46 ((k = e.key) == key || (key != null && key.equals(k)))) 47 break; 48 p = e; 49 } 50 } 51 if (e != null) { // existing mapping for key 52 V oldValue = e.value; 53 /hash值和key相等的狀況下,更新value 54 if (!onlyIfAbsent || oldValue == null) 55 e.value = value; 56 // 57 afterNodeAccess(e); 58 //返回舊的value值 59 return oldValue; 60 } 61 } 62 //修改次數自增 63 ++modCount; 64 //判斷是否須要再次擴容 65 if (++size > threshold) 66 resize(); 67 // 68 afterNodeInsertion(evict); 69 return null; 70 }
上面的代碼即使加了註釋,看上去也不是很清晰,csdn有篇文章分析了hashmap的這個方法,並給出了一個流程圖,邏輯很清晰:
圖片來源:https://blog.csdn.net/lianhuazy167/article/details/66967698
上面put方法的實現中用到了一個很重要的方法resize(),這個方法的做用是當hashmap集合中的元素已經超過最大承載容量時,則對hashmap進行容量擴充。最大裝載容量threshold=capacity*loadFactor,這個值通常小於數組的長度,下面看一下這個方法的實現過程:
1 //初始化或者是擴展table的容量,table的容量時按照2的指數增加的,當擴大table容量時,元素的hash值以及位置可能發生改變 4 final Node<K,V>[] resize() { 5 Node<K,V>[] oldTab = table; 6 //計算當前哈希表 table數組長度 7 int oldCap = (oldTab == null) ? 0 : oldTab.length; 8 //當前閾值(裝載容量=數組長度*裝載因子) 9 int oldThr = threshold; 10 int newCap, newThr = 0; 11 //若是table數組長度大於0 12 if (oldCap > 0) { 13 //table數組長度大於等於hashmap默認的最大值: 2的30次方 14 if (oldCap >= MAXIMUM_CAPACITY) { 15 //擴充爲爲int型最大值 16 threshold = Integer.MAX_VALUE; 17 return oldTab; 18 } 19 //若是table數據長度>=初始化長度(16) 並且 擴展1倍也小於默認最大長度:2的30次方 20 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 21 oldCap >= DEFAULT_INITIAL_CAPACITY) 22 // threshold 閾值擴大一倍 23 newThr = oldThr << 1; // double threshold 24 } 25 //若是原先的裝載容量>0,直接將新容量賦值爲 原先的裝載容量oldThr->oldThreshold 26 else if (oldThr > 0) // initial capacity was placed in threshold 27 newCap = oldThr; 28 else { // zero initial threshold signifies using defaults 29 //原先的閾值oldThr< =0 並且table長度也=0,這說明hashmap還未初始化,執行初始化 30 newCap = DEFAULT_INITIAL_CAPACITY;//數組長度賦值16 31 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 32 } 33 //計算新的閾值上限 34 if (newThr == 0) { 35 float ft = (float)newCap * loadFactor; 36 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 37 (int)ft : Integer.MAX_VALUE); 38 } 39 //更新爲新的閾值 40 threshold = newThr; 41 //從新分配table容量 42 @SuppressWarnings({"rawtypes","unchecked"}) 43 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 44 table = newTab; 45 //把原先table中的複製到新的table中 46 if (oldTab != null) { 47 for (int j = 0; j < oldCap; ++j) { 48 Node<K,V> e; 49 if ((e = oldTab[j]) != null) { 50 oldTab[j] = null; 51 if (e.next == null) 52 newTab[e.hash & (newCap - 1)] = e; 53 else if (e instanceof TreeNode) 54 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 55 else { // preserve order 56 Node<K,V> loHead = null, loTail = null; 57 Node<K,V> hiHead = null, hiTail = null; 58 Node<K,V> next; 59 do { 60 next = e.next; 61 if ((e.hash & oldCap) == 0) { 62 if (loTail == null) 63 loHead = e; 64 else 65 loTail.next = e; 66 loTail = e; 67 } 68 else { 69 if (hiTail == null) 70 hiHead = e; 71 else 72 hiTail.next = e; 73 hiTail = e; 74 } 75 } while ((e = next) != null); 76 if (loTail != null) { 77 loTail.next = null; 78 newTab[j] = loHead; 79 } 80 if (hiTail != null) { 81 hiTail.next = null; 82 newTab[j + oldCap] = hiHead; 83 } 84 } 85 } 86 } 87 } 88 return newTab; 89 }
Hashmap的擴容機制主要實現步驟:
若是當前數組爲空,則初始化當前數組(長度16,裝載因子0.75)
若是當前數組不爲空,則將當前數組容量擴大一倍,同時將閾值(threshold)也擴大一倍,而後將以前table素組中值所有複製到新的table中
經過key獲取對應的value,實現邏輯:根據key計算hash值,經過hash值和key從hashmap中檢索出惟一的結果並返回。
1 public V get(Object key) { 2 Node<K,V> e; 3 return (e = getNode(hash(key), key)) == null ? null : e.value; 4 } 5 //hash:key對應的hash值 6 //key:鍵值對的key 7 final Node<K,V> getNode(int hash, Object key) { 8 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 9 10 if ((tab = table) != null && (n = tab.length) > 0 && 11 (first = tab[(n - 1) & hash]) != null) { // tab[(n - 1) & hash] 12 // 根據hash值計算出table中的位置,若是該位置第一個節點 key 和 hash值都和傳遞進來的參數相等,則返回該Node 13 if (first.hash == hash && // always check first node 14 ((k = first.key) == key || (key != null && key.equals(k)))) 15 return first; 16 //該鍵值對在hash表(n - 1) & hash索引處,可是不是第一個節點,多以遍歷該鏈表(也多是紅黑樹),不論是鏈表仍是樹,順藤摸瓜就對了 17 if ((e = first.next) != null) { 18 //若是是紅黑樹,則遍歷樹型結構 19 if (first instanceof TreeNode) 20 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 21 //不是樹,遍歷鏈表 22 do { 23 if (e.hash == hash && 24 ((k = e.key) == key || (key != null && key.equals(k)))) 25 return e; 26 } while ((e = e.next) != null); 27 } 28 } 29 return null; 30 } 31 32 //計算hash值 33 static final int hash(Object key) { 34 int h; 35 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 36 }
get方法邏輯並無什麼難懂得地方,可是過程當中有兩個地方須要額外注意一下:
tab[(n - 1) & hash]): 根據hash值計算元素位置,其中n爲hashmap中table數組長度,這裏使用(n-1)&hash的方式計算索引位置,簡單解釋一下這個含義,hashmap中數組的大小老是2的指數值,這種特殊的狀況之下(n-1)&hash等同於hash%n取模運算結果,而且使用(n-1)&hash位運算的方式效率上也高於取模運算。
hash(key):計算hash值,這個函數並非直接經過hashCode()獲取hash值,而是作了一步位運算(h = key.hashCode()) ^ (h >>> 16),即將hashcode的高16爲與低16位異或運算,爲何這麼作呢?由於hashcode()返回的是一個32位的int類型數值,將該數值的高16位與低16位作異或運算主要是想讓高位數據參與運算,增長hash值得隨機性。