相關文章
簡介
HashMap最早出現在JDK1.2中,底層基於散列算法實現。HashMap 允許 null 鍵和 null 值,是非線程安全類,在多線程環境下可能會存在問題。
1.8版本的HashMap數據結構:
為什么有的是鏈表有的是紅黑樹?
默認鏈表長度大於8時轉為樹
結構
Node是HhaspMap中的一個靜態內部類 :
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 //構造函數 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 // getter and setter ... toString ... 16 public final K getKey() { return key; } 17 public final V getValue() { return value; } 18 public final String toString() { return key + "=" + value; } 19 20 public final int hashCode() { 21 return Objects.hashCode(key) ^ Objects.hashCode(value); 22 } 23 24 public final V setValue(V newValue) { 25 V oldValue = value; 26 value = newValue; 27 return oldValue; 28 } 29 30 public final boolean equals(Object o) { 31 if (o == this) 32 return true; 33 if (o instanceof Map.Entry) { 34 Map.Entry<?,?> e = (Map.Entry<?,?>)o; 35 if (Objects.equals(key, e.getKey()) && 36 Objects.equals(value, e.getValue())) 37 return true; 38 } 39 return false; 40 } 41 }
TreeNode 是紅黑樹的數據結構。
1 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
2 TreeNode<K,V> parent; // red-black tree links
3 TreeNode<K,V> left; 4 TreeNode<K,V> right; 5 TreeNode<K,V> prev; // needed to unlink next upon deletion 6 boolean red; 7 TreeNode(int hash, K key, V val, Node<K,V> next) { 8 super(hash, key, val, next); 9 } 10 11 /** 12 * Returns root of tree containing this node. 13 */ 14 final TreeNode<K,V> root() { 15 for (TreeNode<K,V> r = this, p;;) { 16 if ((p = r.parent) == null) 17 return r; 18 r = p; 19 } 20 }
類定義
1 public class HashMap<K,V> extends AbstractMap<K,V> 2 implements Map<K,V>, Cloneable, Serializable
變量
1 /**
2 * 默認初始容量16(必須是2的冪次方)
3 */
4 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
5
6 /**
7 * 最大容量,2的30次方
8 */
9 static final int MAXIMUM_CAPACITY = 1 << 30; 10 11 /** 12 * 默認加載因子,用來計算threshold 13 */ 14 static final float DEFAULT_LOAD_FACTOR = 0.75f; 15 16 /** 17 * 鏈表轉成樹的閾值,當桶中鏈表長度大於8時轉成樹 18 threshold = capacity * loadFactor 19 */ 20 static final int TREEIFY_THRESHOLD = 8; 21 22 /** 23 * 進行resize操作時,若桶中數量少於6則從樹轉成鏈表 24 */ 25 static final int UNTREEIFY_THRESHOLD = 6; 26 27 /** 28 * 桶中結構轉化為紅黑樹對應的table的最小大小 29 30 當需要將解決 hash 沖突的鏈表轉變為紅黑樹時, 31 需要判斷下此時數組容量, 32 若是由於數組容量太小(小於 MIN_TREEIFY_CAPACITY ) 33 導致的 hash 沖突太多,則不進行鏈表轉變為紅黑樹操作, 34 轉為利用 resize() 函數對 hashMap 擴容 35 */ 36 static final int MIN_TREEIFY_CAPACITY = 64; 37 /** 38 保存Node<K,V>節點的數組 39 該表在首次使用時初始化,並根據需要調整大小。 分配時, 40 長度始終是2的冪。 41 */ 42 transient Node<K,V>[] table; 43 44 /** 45 * 存放具體元素的集 46 */ 47 transient Set<Map.Entry<K,V>> entrySet; 48 49 /** 50 * 記錄 hashMap 當前存儲的元素的數量 51 */ 52 transient int size; 53 54 /** 55 * 每次更改map結構的計數器 56 */ 57 transient int modCount; 58 59 /** 60 * 臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴容 61 */ 62 int threshold; 63 64 /** 65 * 負載因子:要調整大小的下一個大小值(容量*加載因子)。 66 */ 67 final float loadFactor;
構造方法
1 /**
2 * 傳入初始容量大小,使用默認負載因子值 來初始化HashMap對象
3 */
4 public HashMap(int initialCapacity) {
5 this(initialCapacity, DEFAULT_LOAD_FACTOR); 6 } 7 8 /** 9 * 默認容量和負載因子 10 */ 11 public HashMap() { 12 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted 13 } 14 /** 15 * 傳入初始容量大小和負載因子 來初始化HashMap對象 16 */ 17 public HashMap(int initialCapacity, float loadFactor) { 18 // 初始容量不能小於0,否則報錯 19 if (initialCapacity < 0) 20 throw new IllegalArgumentException("Illegal initial capacity: " + 21 initialCapacity); 22 // 初始容量不能大於最大值,否則為最大值 23 if (initialCapacity > MAXIMUM_CAPACITY) 24 initialCapacity = MAXIMUM_CAPACITY; 25 //負載因子不能小於或等於0,不能為非數字 26 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 27 throw new IllegalArgumentException("Illegal load factor: " + 28 loadFactor); 29 // 初始化負載因子 30 this.loadFactor = loadFactor; 31 // 初始化threshold大小 32 this.threshold = tableSizeFor(initialCapacity); 33 } 34 35 /** 36 * 找到大於或等於 cap 的最小2的整數次冪的數。 37 */ 38 static final int tableSizeFor(int cap) { 39 int n = cap - 1; 40 n |= n >>> 1; 41 n |= n >>> 2; 42 n |= n >>> 4; 43 n |= n >>> 8; 44 n |= n >>> 16; 45 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 46 }
tableSizeFor方法詳解:
用位運算找到大於或等於 cap 的最小2的整數次冪的數。比如10,則返回16
-
讓cap-1再賦值給n的目的是使得找到的目標值大於或等於原值。例如二進制
0100
,十進制是4,若不減1而直接操作,答案是0001 0000
十進制是16,明顯不符合預期。 -
對n右移1位:001xx…xxx,再位或:011xx…xxx
-
對n右移2位:00011…xxx,再位或:01111…xxx
-
對n右移4位…
-
對n右移8位…
-
對n右移16位,因為int最大就
2^32
所以移動1、2、4、8、16位並取位或,會將最高位的1后面的位全變為1。 -
再讓結果n+1,即得到了2的整數次冪的值了。
附帶一個實例:
loadFactor 負載因子
對於 HashMap 來說,負載因子是一個很重要的參數,該參數反應了 HashMap 桶數組的使用情況。通過調節負載因子,可使 HashMap 時間和空間復雜度上有不同的表現。
當我們調低負載因子時,HashMap 所能容納的鍵值對數量變少。擴容時,重新將鍵值對存儲新的桶數組里,鍵的鍵之間產生的碰撞會下降,鏈表長度變短。此時,HashMap 的增刪改查等操作的效率將會變高,這里是典型的拿空間換時間。
相反,如果增加負載因子(負載因子可以大於1),HashMap 所能容納的鍵值對數量變多,空間利用率高,但碰撞率也高。這意味着鏈表長度變長,效率也隨之降低,這種情況是拿時間換空間。至於負載因子怎么調節,這個看使用場景了。
一般情況下,我們用默認值就可以了。大多數情況下0.75在時間跟空間代價上達到了平衡所以不建議修改。
查找
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值 6 static final int hash(Object key) { 7 int h; 8 // 拿到key的hash值后與其五符號右移16位取與 9 // 通過這種方式,讓高位數據與低位數據進行異或,以此加大低位信息的隨機性,變相的讓高位數據參與到計算中。 10 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 11 } 12 13 final Node<K,V> getNode(int hash, Object key) { 14 Node<K,V>[] tab; 15 Node<K,V> first, e; 16 int n; K k; 17 // 定位鍵值對所在桶的位置 18 if ((tab = table) != null && (n = tab.length) > 0 && 19 (first = tab[(n - 1) & hash]) != null) { 20 // 判斷桶中第一項(數組元素)相等 21 if (first.hash == hash && // always check first node 22 ((k = first.key) == key || (key != null && key.equals(k)))) 23 return first; 24 // 桶中不止一個結點 25 if ((e = first.next) != null) { 26 // 是否是紅黑樹,是的話調用getTreeNode方法 27 if (first instanceof TreeNode) 28 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 29 // 不是紅黑樹的話,在鏈表中遍歷查找 30 do { 31 if (e.hash == hash && 32 ((k = e.key) == key || (key != null && key.equals(k)))) 33 return e; 34 } while ((e = e.next) != null); 35 } 36 } 37 return null; 38 }
注意:
-
HashMap的hash算法(
hash()
方法)。 -
(n - 1) & hash
等價於對 length 取余。
添加
1 public V put(K key, V value) {
2 // 調用hash(key)方法來計算hash
3 return putVal(hash(key), key, value, false, true); 4 } 5 6 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 7 boolean evict) { 8 Node<K,V>[] tab; 9 Node<K,V> p; 10 int n, i; 11 // 容量初始化:當table為空,則調用resize()方法來初始化容器 12 if ((tab = table) == null || (n = tab.length) == 0) 13 n = (tab = resize()).length; 14 //確定元素存放在哪個桶中,桶為空,新生成結點放入桶中 15 if ((p = tab[i = (n - 1) & hash]) == null) 16 tab[i] = newNode(hash, key, value, null); 17 else { 18 Node<K,V> e; K k; 19 // 比較桶中第一個元素(數組中的結點)的hash值相等,key相等 20 if (p.hash == hash && 21 ((k = p.key) == key || (key != null && key.equals(k)))) 22 //如果鍵的值以及節點 hash 等於鏈表中的第一個鍵值對節點時,則將 e 指向該鍵值對 23 e = p; 24 // 如果桶中的引用類型為 TreeNode,則調用紅黑樹的插入方法 25 else if (p instanceof TreeNode) 26 // 放入樹中 27 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 28 else { 29 //對鏈表進行遍歷,並統計鏈表長度 30 for (int binCount = 0; ; ++binCount) { 31 // 到達鏈表的尾部 32 if ((e = p.next) == null) { 33 //在尾部插入新結點 34 p.next = newNode(hash, key, value, null); 35 // 如果結點數量達到閾值,轉化為紅黑樹 36 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 37 treeifyBin(tab, hash); 38 break; 39 } 40 // 判斷鏈表中結點的key值與插入的元素的key值是否相等 41 if (e.hash == hash && 42 ((k = e.key) == key || (key != null && key.equals(k)))) 43 break; 44 p = e; 45 } 46 } 47 //判斷要插入的鍵值對是否存在 HashMap 中 48 if (e != null) { // existing mapping for key 49 V oldValue = e.value; 50 // onlyIfAbsent 表示是否僅在 oldValue 為 null 的情況下更新鍵值對的值 51 if (!onlyIfAbsent || oldValue == null) 52 e.value = value; 53 afterNodeAccess(e); 54 return oldValue; 55 } 56 } 57 ++modCount; 58 // 鍵值對數量超過閾值時,則進行擴容 59 if (++size > threshold) 60 resize(); 61 afterNodeInsertion(evict); 62 return null; 63 }
事實上,new HashMap();
完成后,如果沒有put
操作,是不會分配存儲空間的。
-
當桶數組 table 為空時,通過擴容的方式初始化 table
-
查找要插入的鍵值對是否已經存在,存在的話根據條件判斷是否用新值替換舊值
-
如果不存在,則將鍵值對鏈入鏈表中,並根據鏈表長度決定是否將鏈表轉為紅黑樹
-
判斷鍵值對數量是否大於閾值,大於的話則進行擴容操作
擴容機制
在 HashMap 中,桶數組的長度均是2的冪,閾值大小為桶數組長度與負載因子的乘積。當 HashMap 中的鍵值對數量超過閾值時,進行擴容。
HashMap 按當前桶數組長度的2倍進行擴容,閾值也變為原來的2倍(如果計算過程中,閾值溢出歸零,則按閾值公式重新計算)。擴容之后,要重新計算鍵值對的位置,並把它們移動到合適的位置上去。
1 final Node<K,V>[] resize() {
2 // 拿到數組桶
3 Node<K,V>[] oldTab = table; 4 int oldCap = (oldTab == null) ? 0 : oldTab.length; 5 int oldThr = threshold; 6 int newCap, newThr = 0; 7 // 如果數組桶的容量大與0 8 if (oldCap > 0) { 9 // 如果比最大值還大,則賦值為最大值 10 if (oldCap >= MAXIMUM_CAPACITY) { 11 threshold = Integer.MAX_VALUE; 12 return oldTab; 13 } 14 // 如果擴容后小於最大值 而且 舊數組桶大於初始容量16, 閾值左移1(擴大2倍) 15 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 16 oldCap >= DEFAULT_INITIAL_CAPACITY) 17 newThr = oldThr << 1; // double threshold 18 } 19 // 如果數組桶容量<=0 且 舊閾值 >0 20 else if (oldThr > 0) // initial capacity was placed in threshold 21 // 新容量=舊閾值 22 newCap = oldThr; 23 // 如果數組桶容量<=0 且 舊閾值 <=0 24 else { // zero initial threshold signifies using defaults 25 // 新容量=默認容量 26 newCap = DEFAULT_INITIAL_CAPACITY; 27 // 新閾值= 負載因子*默認容量 28 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 29 } 30 // 如果新閾值為0 31 if (newThr == 0) { 32 // 重新計算閾值 33 float ft = (float)newCap * loadFactor; 34 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 35 (int)ft : Integer.MAX_VALUE); 36 } 37 // 更新閾值 38 threshold = newThr; 39 @SuppressWarnings({"rawtypes","unchecked"}) 40 // 創建新數組 41 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 42 // 覆蓋數組桶 43 table = newTab; 44 // 如果舊數組桶不是空,則遍歷桶數組,並將鍵值對映射到新的桶數組中 45 if (oldTab != null) { 46 for (int j = 0; j < oldCap; ++j) { 47 Node<K,V> e; 48 if ((e = oldTab[j]) != null) { 49 oldTab[j] = null; 50 if (e.next == null) 51 newTab[e.hash & (newCap - 1)] = e; 52 // 如果是紅黑樹 53 else if (e instanceof TreeNode) 54 // 重新映射時,需要對紅黑樹進行拆分 55 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 56 else { // preserve order 57 // 如果不是紅黑樹,則按鏈表處理 58 Node<K,V> loHead = null, loTail = null; 59 Node<K,V> hiHead = null, hiTail = null; 60 Node<K,V> next; 61 // 遍歷鏈表,並將鏈表節點按原順序進行分組 62 do { 63 next = e.next; 64 if ((e.hash & oldCap) == 0) { 65 if (loTail == null) 66 loHead = e; 67 else 68 loTail.next = e; 69 loTail = e; 70 } 71 else { 72 if (hiTail == null) 73 hiHead = e; 74 else 75 hiTail.next = e; 76 hiTail = e; 77 } 78 } while ((e = next) != null); 79 // 將分組后的鏈表映射到新桶中 80 if (loTail != null) { 81 loTail.next = null; 82 newTab[j] = loHead; 83 } 84 if (hiTail != null) { 85 hiTail.next = null; 86 newTab[j + oldCap] = hiHead; 87 } 88 } 89 } 90 } 91 } 92 return newTab; 93 }
整體步驟:
-
計算新桶數組的容量 newCap 和新閾值 newThr
-
根據計算出的 newCap 創建新的桶數組,桶數組 table 也是在這里進行初始化的
-
將鍵值對節點重新映射到新的桶數組里。如果節點是 TreeNode 類型,則需要拆分紅黑樹。如果是普通節點,則節點按原順序進行分組。
總結起來,一共有三種擴容方式:
-
使用默認構造方法初始化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
量為原有的兩倍。
細心點的人會很好奇,為什么要判斷loadFactor為0呢?
loadFactor小數位為 0,整數位可被2整除且大於等於8時,在某次計算中就可能會導致 newThr 溢出歸零。
疑問和進階
1. JDK1.7是基於數組+單鏈表實現(為什么不用雙鏈表)
首先,用鏈表是為了解決hash沖突。
單鏈表能實現為什么要用雙鏈表呢?(雙鏈表需要更大的存儲空間)
2. 為什么要用紅黑樹,而不用平衡二叉樹?
插入效率比平衡二叉樹高,查詢效率比普通二叉樹高。所以選擇性能相對折中的紅黑樹。
3. 重寫對象的Equals方法時,要重寫hashCode方法,為什么?跟HashMap有什么關系?
equals與hashcode間的關系:
-
如果兩個對象相同(即用equals比較返回true),那么它們的hashCode值一定要相同;
-
如果兩個對象的hashCode相同,它們並不一定相同(即用equals比較返回false)
因為在 HashMap 的鏈表結構中遍歷判斷的時候,特定情況下重寫的 equals 方法比較對象是否相等的業務邏輯比較復雜,循環下來更是影響查找效率。所以這里把 hashcode 的判斷放在前面,只要 hashcode 不相等就玩兒完,不用再去調用復雜的 equals 了。很多程度地提升 HashMap 的使用效率。
所以重寫 hashcode 方法是為了讓我們能夠正常使用 HashMap 等集合類,因為 HashMap 判斷對象是否相等既要比較 hashcode 又要使用 equals 比較。而這樣的實現是為了提高 HashMap 的效率。
附上源碼圖:
4. HashMap為什么不直接使用對象的原始hash值呢?
1 static final int hash(Object key) {
2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 4 }
我們發現,HashMap的哈希值是通過上面的方式獲取,而不是通過key.hashCode()
方法獲取。
原因:
通過移位和異或運算,可以讓 hash 變得更復雜,進而影響 hash 的分布性。
5. 既然紅黑樹那么好,為啥hashmap不直接采用紅黑樹,而是當大於8個的時候才轉換紅黑樹?
因為紅黑樹需要進行左旋,右旋操作, 而單鏈表不需要。
以下都是單鏈表與紅黑樹結構對比。
如果元素小於8個,查詢成本高,新增成本低。
如果元素大於8個,查詢成本低,新增成本高。
至於為什么選數字8,是大佬折中衡量的結果-.-,就像loadFactor默認值0.75一樣。