接下來會從如下幾個方面介紹 HashMap 源碼相關知識:node
一、HashMap 存儲結構算法
二、HashMap 各常量、成員變量做用數組
三、HashMap 幾種構造方法安全
四、HashMap put 及其相關方法數據結構
五、HashMap get 及其相關方法ide
六、HashMap remove 及其相關方法函數
七、HashMap 擴容方法 resize()this
介紹方法時會包含方法實現相關細節。線程
先來看一下 HashMap 的繼承圖:3d
HashMap 根據鍵的 hashCode 值存儲數據,大多數狀況下能夠直接定位到它的值,於是具備很快的訪問速度,但遍歷順序倒是不肯定的。 HashMap 最多隻容許一條記錄的鍵爲 null ,容許多條記錄的值爲 null 。HashMap 非線程安全,即任一時刻能夠有多個線程同時寫 HashMap,可能會致使數據的不一致。若是須要知足線程安全,能夠用 Collections的synchronizedMap 方法使 HashMap 具備線程安全的能力,或者使用ConcurrentHashMap。
1、HashMap 存儲結構
HashMap是數組+鏈表+紅黑樹(JDK1.8增長了紅黑樹部分)實現的,以下圖所示:
源碼中具體實現以下:
1 // Node<K,V> 類用來實現數組及鏈表的數據結構 2 static class Node<K,V> implements Map.Entry<K,V> { 3 final int hash; //保存節點的 hash 值 4 final K key; //保存節點的 key 值 5 V value; //保存節點的 value 值 6 Node<K,V> next; //指向鏈表結構下的當前節點的 next 節點,紅黑樹 TreeNode 節點中也有用到 7 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() { } 16 public final V getValue() { } 17 public final String toString() { } 18 19 public final int hashCode() { 20 } 21 22 public final V setValue(V newValue) { 23 } 24 25 public final boolean equals(Object o) { 26 } 27 } 28 29 public class LinkedHashMap<K,V> { 30 static class Entry<K,V> extends HashMap.Node<K,V> { 31 Entry<K,V> before, after; 32 Entry(int hash, K key, V value, Node<K,V> next) { 33 super(hash, key, value, next); 34 } 35 } 36 } 37 38 // TreeNode<K,V> 繼承 LinkedHashMap.Entry<K,V>,用來實現紅黑樹相關的存儲結構 39 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { 40 TreeNode<K,V> parent; // 存儲當前節點的父節點 41 TreeNode<K,V> left; //存儲當前節點的左孩子 42 TreeNode<K,V> right; //存儲當前節點的右孩子 43 TreeNode<K,V> prev; // 存儲當前節點的前一個節點 44 boolean red; // 存儲當前節點的顏色(紅、黑) 45 TreeNode(int hash, K key, V val, Node<K,V> next) { 46 super(hash, key, val, next); 47 } 48 }
2、HashMap 各常量、成員變量做用
1 //建立 HashMap 時未指定初始容量狀況下的默認容量 2 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 3 4 //HashMap 的最大容量 5 static final int MAXIMUM_CAPACITY = 1 << 30; 6 7 //HashMap 默認的裝載因子,當 HashMap 中元素數量超過 容量*裝載因子 時,進行 resize() 操做 8 static final float DEFAULT_LOAD_FACTOR = 0.75f; 9 10 //用來肯定什麼時候將解決 hash 衝突的鏈表轉變爲紅黑樹 11 static final int TREEIFY_THRESHOLD = 8; 12 13 // 用來肯定什麼時候將解決 hash 衝突的紅黑樹轉變爲鏈表 14 static final int UNTREEIFY_THRESHOLD = 6; 15 16 /* 當須要將解決 hash 衝突的鏈表轉變爲紅黑樹時,須要判斷下此時數組容量,如果因爲數組容量過小(小於 MIN_TREEIFY_CAPACITY )致使的 hash 衝突太多,則不進行鏈表轉變爲紅黑樹操做,轉爲利用 resize() 函數對 hashMap 擴容 */ 17 static final int MIN_TREEIFY_CAPACITY = 64;
1 //保存Node<K,V>節點的數組 2 transient Node<K,V>[] table; 3 4 //由 hashMap 中 Node<K,V> 節點構成的 set 5 transient Set<Map.Entry<K,V>> entrySet; 6 7 //記錄 hashMap 當前存儲的元素的數量 8 transient int size; 9 10 //記錄 hashMap 發生結構性變化的次數(注意 value 的覆蓋不屬於結構性變化) 11 transient int modCount; 12 13 //threshold的值應等於 table.length * loadFactor, size 超過這個值時進行 resize()擴容 14 int threshold; 15 16 //記錄 hashMap 裝載因子 17 final float loadFactor;
3、HashMap 幾種構造方法
1 //構造方法1,指定初始容量及裝載因子 2 public HashMap(int initialCapacity, float loadFactor) { 3 if (initialCapacity < 0) 4 throw new IllegalArgumentException("Illegal initial capacity: " + 5 initialCapacity); 6 if (initialCapacity > MAXIMUM_CAPACITY) 7 initialCapacity = MAXIMUM_CAPACITY; 8 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 9 throw new IllegalArgumentException("Illegal load factor: " + 10 loadFactor); 11 this.loadFactor = loadFactor; 12 /* tableSizeFor(initialCapacity) 方法返回的值是最接近 initialCapacity 的2的冪,若指定初始容量爲9,則實際 hashMap 容量爲16*/ 13 //注意此種方法建立的 hashMap 初始容量的值存在 threshold 中 14 this.threshold = tableSizeFor(initialCapacity); 15 } 16 //tableSizeFor(initialCapacity) 方法返回的值是最接近 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 } 26 //構造方法2,僅指定初始容量,裝載因子的值採用默認的 0.75 27 public HashMap(int initialCapacity) { 28 this(initialCapacity, DEFAULT_LOAD_FACTOR); 29 } 30 //構造方法3,全部參數均採用默認值 31 public HashMap() { 32 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 33 }
4、HashMap put 及其相關方法
這部分我以爲是 hashMap 中比較重要的代碼,介紹以下:
1 //指定節點 key,value,向 hashMap 中插入節點 2 public V put(K key, V value) { 3 //注意待插入節點 hash 值的計算,調用了 hash(key) 函數 4 //實際調用 putVal()進行節點的插入 5 return putVal(hash(key), key, value, false, true); 6 } 7 static final int hash(Object key) { 8 int h; 9 /*key 的 hash 值的計算是經過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這麼作能夠在數組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷*/ 10 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 11 } 12 13 public void putAll(Map<? extends K, ? extends V> m) { 14 putMapEntries(m, true); 15 } 16 17 /*把Map<? extends K, ? extends V> m 中的元素插入到 hashMap 中,若 evict 爲 false,表明是在建立 hashMap 時調用了這個函數,例如利用上述構造函數3建立 hashMap;若 evict 爲true,表明是在建立 hashMap 後才調用這個函數,例如上述的 putAll 函數。*/ 18 19 final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { 20 int s = m.size(); 21 if (s > 0) { 22 /*若是是在建立 hashMap 時調用的這個函數則 table 必定爲空*/ 23 if (table == null) { 24 //根據待插入的map 的 size 計算要建立的 hashMap 的容量。 25 float ft = ((float)s / loadFactor) + 1.0F; 26 int t = ((ft < (float)MAXIMUM_CAPACITY) ? 27 (int)ft : MAXIMUM_CAPACITY); 28 //把要建立的 hashMap 的容量存在 threshold 中 29 if (t > threshold) 30 threshold = tableSizeFor(t); 31 } 32 //判斷待插入的 map 的 size,若 size 大於 threshold,則先進行 resize() 33 else if (s > threshold) 34 resize(); 35 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { 36 K key = e.getKey(); 37 V value = e.getValue(); 38 //實際也是調用 putVal 函數進行元素的插入 39 putVal(hash(key), key, value, false, evict); 40 } 41 } 42 } 43 44 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 45 boolean evict) { 46 Node<K,V>[] tab; Node<K,V> p; int n, i; 47 if ((tab = table) == null || (n = tab.length) == 0) 48 n = (tab = resize()).length; 49 /*根據 hash 值肯定節點在數組中的插入位置,若此位置沒有元素則進行插入,注意肯定插入位置所用的計算方法爲 (n - 1) & hash,因爲 n 必定是2的冪次,這個操做至關於 50 hash % n */ 51 if ((p = tab[i = (n - 1) & hash]) == null) 52 tab[i] = newNode(hash, key, value, null); 53 else {//說明待插入位置存在元素 54 Node<K,V> e; K k; 55 //比較原來元素與待插入元素的 hash 值和 key 值 56 if (p.hash == hash && 57 ((k = p.key) == key || (key != null && key.equals(k)))) 58 e = p; 59 //若原來元素是紅黑樹節點,調用紅黑樹的插入方法:putTreeVal 60 else if (p instanceof TreeNode) 61 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 62 else {//證實原來的元素是鏈表的頭結點,今後節點開始向後尋找合適插入位置 63 for (int binCount = 0; ; ++binCount) { 64 if ((e = p.next) == null) { 65 //找到插入位置後,新建節點插入 66 p.next = newNode(hash, key, value, null); 67 //若鏈表上節點超過TREEIFY_THRESHOLD - 1,將鏈表變爲紅黑樹 68 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 69 treeifyBin(tab, hash); 70 break; 71 } 72 if (e.hash == hash && 73 ((k = e.key) == key || (key != null && key.equals(k)))) 74 break; 75 p = e; 76 } 77 }//end else 78 if (e != null) { // 待插入元素在 hashMap 中已存在 79 V oldValue = e.value; 80 if (!onlyIfAbsent || oldValue == null) 81 e.value = value; 82 afterNodeAccess(e); 83 return oldValue; 84 } 85 }//end else 86 ++modCount; 87 if (++size > threshold) 88 resize(); 89 afterNodeInsertion(evict); 90 return null; 91 }//end putval
1 /*讀懂這個函數要注意理解 hash 衝突發生的幾種狀況 2 1、兩節點 key 值相同(hash值必定相同),致使衝突 3 2、兩節點 key 值不一樣,因爲 hash 函數的侷限性致使hash 值相同,衝突 4 3、兩節點 key 值不一樣,hash 值不一樣,但 hash 值對數組長度取模後相同,衝突 5 */ 6 final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, 7 int h, K k, V v) { 8 Class<?> kc = null; 9 boolean searched = false; 10 TreeNode<K,V> root = (parent != null) ? root() : this; 11 //從根節點開始查找合適的插入位置(與二叉搜索樹查找過程相同) 12 for (TreeNode<K,V> p = root;;) { 13 int dir, ph; K pk; 14 if ((ph = p.hash) > h) 15 dir = -1; // dir小於0,接下來查找當前節點左孩子 16 else if (ph < h) 17 dir = 1; // dir大於0,接下來查找當前節點右孩子 18 else if ((pk = p.key) == k || (pk != null && k.equals(pk))) 19 //進入這個else if 表明 hash 值相同,key 相同 20 return p; 21 /*要進入下面這個else if,表明有如下幾個含義: 22 一、當前節點與待插入節點 key 不一樣, hash 值相同 23 二、k是不可比較的,即k並未實現 comparable<K> 接口 (若 k 實現了comparable<K> 接口,comparableClassFor(k)返回的是k的 class,而不是 null) 24 或者 compareComparables(kc, k, pk) 返回值爲 0 (pk 爲空 或者 按照 k.compareTo(pk) 返回值爲0, 返回值爲0多是因爲 k的compareTo 方法實現不當引發的,compareTo 斷定相等,而上個 else if 中 equals 斷定不等)*/ 25 else if ((kc == null && 26 (kc = comparableClassFor(k)) == null) || 27 (dir = compareComparables(kc, k, pk)) == 0) { 28 //在以當前節點爲根的整個樹上搜索是否存在待插入節點(只會搜索一次) 29 if (!searched) { 30 TreeNode<K,V> q, ch; 31 searched = true; 32 if (((ch = p.left) != null && 33 (q = ch.find(h, k, kc)) != null) || 34 ((ch = p.right) != null && 35 (q = ch.find(h, k, kc)) != null)) 36 //若樹中存在待插入節點,直接返回 37 return q; 38 } 39 // 既然k是不可比較的,那我本身指定一個比較方式 40 dir = tieBreakOrder(k, pk); 41 }//end else if 42 43 TreeNode<K,V> xp = p; 44 if ((p = (dir <= 0) ? p.left : p.right) == null) { 45 //找到了待插入的位置,xp 爲待插入節點的父節點 46 //注意TreeNode節點中既存在樹狀關係,也存在鏈式關係,而且是雙端鏈表 47 Node<K,V> xpn = xp.next; 48 TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); 49 if (dir <= 0) 50 xp.left = x; 51 else 52 xp.right = x; 53 xp.next = x; 54 x.parent = x.prev = xp; 55 if (xpn != null) 56 ((TreeNode<K,V>)xpn).prev = x; 57 //插入節點後進行二叉樹的平衡操做 58 moveRootToFront(tab, balanceInsertion(root, x)); 59 return null; 60 } 61 }//end for 62 }//end putTreeVal 63 64 static int tieBreakOrder(Object a, Object b) { 65 int d; 66 //System.identityHashCode()實際是利用對象 a,b 的內存地址進行比較 67 if (a == null || b == null || 68 (d = a.getClass().getName(). 69 compareTo(b.getClass().getName())) == 0) 70 d = (System.identityHashCode(a) <= System.identityHashCode(b) ? 71 -1 : 1); 72 return d; 73 }
5、HashMap get 及其相關方法
1 public V get(Object key) { 2 Node<K,V> e; 3 //其實是根據輸入節點的 hash 值和 key 值利用getNode 方法進行查找 4 return (e = getNode(hash(key), key)) == null ? null : e.value; 5 } 6 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 if ((tab = table) != null && (n = tab.length) > 0 && 10 (first = tab[(n - 1) & hash]) != null) { 11 if (first.hash == hash && // always check first node 12 ((k = first.key) == key || (key != null && key.equals(k)))) 13 return first; 14 if ((e = first.next) != null) { 15 if (first instanceof TreeNode) 16 //若定位到的節點是 TreeNode 節點,則在樹中進行查找 17 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 18 do {//不然在鏈表中進行查找 19 if (e.hash == hash && 20 ((k = e.key) == key || (key != null && key.equals(k)))) 21 return e; 22 } while ((e = e.next) != null); 23 } 24 } 25 return null; 26 }
1 final TreeNode<K,V> getTreeNode(int h, Object k) { 2 //從根節點開始,調用 find 方法進行查找 3 return ((parent != null) ? root() : this).find(h, k, null); 4 } 5 6 final TreeNode<K,V> find(int h, Object k, Class<?> kc) { 7 TreeNode<K,V> p = this; 8 do { 9 int ph, dir; K pk; 10 TreeNode<K,V> pl = p.left, pr = p.right, q; 11 //首先進行hash 值的比較,若不一樣令當前節點變爲它的左孩子或者右孩子 12 if ((ph = p.hash) > h) 13 p = pl; 14 else if (ph < h) 15 p = pr; 16 //hash 值相同,進行 key 值的比較 17 else if ((pk = p.key) == k || (k != null && k.equals(pk))) 18 return p; 19 else if (pl == null) 20 p = pr; 21 else if (pr == null) 22 p = pl; 23 //執行到這兒,意味着hash 值相同,key 值不一樣 24 //若k 是可比較的而且k.compareTo(pk) 返回結果不爲0可進入下面elseif 25 else if ((kc != null || 26 (kc = comparableClassFor(k)) != null) && 27 (dir = compareComparables(kc, k, pk)) != 0) 28 p = (dir < 0) ? pl : pr; 29 /*若 k 是不可比較的 或者 k.compareTo(pk) 返回結果爲0則在整棵樹中進行查找,先找右子樹,右子樹沒有再找左子樹*/ 30 else if ((q = pr.find(h, k, kc)) != null) 31 return q; 32 else 33 p = pl; 34 } while (p != null); 35 return null; 36 }
7、HashMap 擴容方法 resize()
resize() 方法中比較重要的是鏈表和紅黑樹的 rehash 操做,先來講下 rehash 的實現原理:
咱們在擴容的時候,通常是把長度擴爲原來2倍,因此,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。看下圖能夠明白這句話的意思,n爲table的長度,圖(a)表示擴容前的key1和key2兩種key肯定索引位置的示例,圖(b)表示擴容後key1和key2兩種key肯定索引位置的示例,其中hash1是key1對應的哈希與高位運算結果。
元素在從新計算hash以後,由於n變爲2倍,那麼n-1的mask範圍在高位多1bit(紅色),所以新的index就會發生這樣的變化:
所以,咱們在擴充HashMap的時候,只須要看看原來的hash值新增的那個bit是1仍是0就行了,是0的話索引沒變,是1的話索引變成「原索引+oldCap」,能夠看看下圖爲16擴充爲32的resize示意圖:
這個算法很巧妙,既省去了從新計算hash值的時間,並且同時,因爲新增的1bit是0仍是1能夠認爲是隨機的,所以resize的過程,均勻的把以前的衝突的節點分散到新的槽中了。
具體源碼介紹:
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; /* 一、resize()函數在size > threshold時被調用。 oldCap大於 0 表明原來的 table 表非空, oldCap 爲原表的大小, oldThr(threshold) 爲 oldCap × load_factor */ if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } /* 二、resize()函數在table爲空被調用。 oldCap 小於等於 0 且 oldThr 大於0,表明用戶建立了一個 HashMap,可是使用的構造函數爲 HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity) 或 HashMap(Map<? extends K, ? extends V> m),致使 oldTab 爲 null,oldCap 爲0, oldThr 爲用戶指定的 HashMap的初始容量。 */ else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; /* 三、resize()函數在table爲空被調用。 oldCap 小於等於 0 且 oldThr 等於0,用戶調用 HashMap()構造函數建立的 HashMap,全部值均採用默認值, oldTab(Table)表爲空,oldCap爲0,oldThr等於0, */ else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { //把 oldTab 中的節點 reHash 到 newTab 中去 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; //若節點是單個節點,直接在 newTab 中進行重定位 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //若節點是 TreeNode 節點,要進行 紅黑樹的 rehash 操做 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //如果鏈表,進行鏈表的 rehash 操做 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 判斷節點位置 rehash 後是否發生改變 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; // rehash 後節點新的位置必定爲原來基礎上加上 oldCap newTab[j + oldCap] = hiHead; } } } } } return newTab; }
1 //這個函數的功能是對紅黑樹進行 rehash 操做 2 final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) { 3 TreeNode<K,V> b = this; 4 // Relink into lo and hi lists, preserving order 5 TreeNode<K,V> loHead = null, loTail = null; 6 TreeNode<K,V> hiHead = null, hiTail = null; 7 int lc = 0, hc = 0; 8 //因爲 TreeNode 節點之間存在雙端鏈表的關係,能夠利用鏈表關係進行 rehash 9 for (TreeNode<K,V> e = b, next; e != null; e = next) { 10 next = (TreeNode<K,V>)e.next; 11 e.next = null; 12 if ((e.hash & bit) == 0) { 13 if ((e.prev = loTail) == null) 14 loHead = e; 15 else 16 loTail.next = e; 17 loTail = e; 18 ++lc; 19 } 20 else { 21 if ((e.prev = hiTail) == null) 22 hiHead = e; 23 else 24 hiTail.next = e; 25 hiTail = e; 26 ++hc; 27 } 28 } 29 30 //rehash 操做以後注意對根據鏈表長度進行 untreeify 或 treeify 操做 31 if (loHead != null) { 32 if (lc <= UNTREEIFY_THRESHOLD) 33 tab[index] = loHead.untreeify(map); 34 else { 35 tab[index] = loHead; 36 if (hiHead != null) // (else is already treeified) 37 loHead.treeify(tab); 38 } 39 } 40 if (hiHead != null) { 41 if (hc <= UNTREEIFY_THRESHOLD) 42 tab[index + bit] = hiHead.untreeify(map); 43 else { 44 tab[index + bit] = hiHead; 45 if (loHead != null) 46 hiHead.treeify(tab); 47 } 48 }//end if 49 }//end split 50
關於 HashMap 源碼閱讀的相關知識就先介紹到這裏,有一些地方還須要更深刻理解透徹(例如紅黑樹的插入節點以後的平衡操做,刪除節點操做),後期會繼續補充。