注意:本文基於JDK1.8進行記錄。
1 簡介
不允許插入key值相同的元素,允許插入null的key值。
底層由數組、鏈表、紅黑樹組成,數組中存儲鏈表或紅黑樹,將一個key到value的映射作為一個元素,不能保證插入順序和輸出順序一致。
線程不安全。
2 擴容機制
數組結構會有容量的概念,HashMap的默認容量為16,默認負載因子是0.75,表示當插入元素后個數超出長度的0.75倍時會進行擴增,默認擴容增量是1,所以擴增后容量為2倍。
最好指定初始容量值,避免過多的進行擴容操作而浪費時間和效率。
3 方法說明
3.1 構造方法
1 // 指定長度和負載因子的構造器。 2 public HashMap(int initialCapacity, float loadFactor); 3 // 指定長度的構造器,使用默認負載因子。 4 public HashMap(int initialCapacity); 5 // 空參構造器,使用默認負載因子。 6 public HashMap(); 7 // 傳入了一個集合的構造器,使用默認負載因子,添加指定集合。 8 public HashMap(Map<? extends K, ? extends V> m);
3.2 常用方法
1 // 獲取個數。 2 public int size(); 3 // 判斷是否為空。 4 public boolean isEmpty(); 5 // 根據key獲取value,不存在會返回null。 6 public V get(Object key); 7 // 設置key和value鍵值對,返回原value,不存在會返回null。 8 public V put(K key, V value); 9 // 根據key刪除鍵值對,返回原value,不存在會返回null。 10 public V remove(Object key); 11 // 清除所有元素。 12 public void clear();
4 源碼分析
4.1 屬性
靜態屬性:
1 // 默認容量為16,是2的整數次冪。 2 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 3 // 最大容量是2的30次方,傳入容量過大將被這個值替換。 4 static final int MAXIMUM_CAPACITY = 1 << 30; 5 // 默認負載因子為0.75。 6 static final float DEFAULT_LOAD_FACTOR = 0.75f; 7 // 樹化閾值為8,鏈表中元素的個數超過8時會轉換為紅黑樹 8 static final int TREEIFY_THRESHOLD = 8; 9 // 反樹化閾值為6,紅黑樹中元素的個數小於6時會轉換為鏈表。 10 static final int UNTREEIFY_THRESHOLD = 6; 11 // 樹化時哈希表最小的容量為64。為了避免沖突,該值至少為樹化閾值和4的乘積。 12 static final int MIN_TREEIFY_CAPACITY = 64;
普通屬性:
1 // 數組,用於存儲鏈表和紅黑樹。 2 transient Node<K,V>[] table; 3 // 存儲key和value鍵值對的集合。 4 transient Set<Map.Entry<K,V>> entrySet; 5 // 鍵值對的個數。 6 transient int size; 7 // 修改次數,用於快速失敗機制。 8 transient int modCount; 9 // 擴容閾值。 10 int threshold; 11 // 負載因子。 12 final float loadFactor;
4.2 工具方法
1 // 根據key的hashCode重新計算hash值。 2 static final int hash(Object key) { 3 int h; 4 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 5 } 6 // 根據長度計算閾值。 7 static final int tableSizeFor(int cap) { 8 int n = cap - 1; 9 n |= n >>> 1; 10 n |= n >>> 2; 11 n |= n >>> 4; 12 n |= n >>> 8; 13 n |= n >>> 16; 14 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 15 }
4.3 構造方法
1 // 指定長度和負載因子的構造器。 2 public HashMap(int initialCapacity, float loadFactor) { 3 if (initialCapacity < 0) 4 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); 5 if (initialCapacity > MAXIMUM_CAPACITY) 6 initialCapacity = MAXIMUM_CAPACITY; 7 if (loadFactor <= 0 || Float.isNaN(loadFactor)) 8 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); 9 this.loadFactor = loadFactor; 10 // 根據長度設置閾值。 11 this.threshold = tableSizeFor(initialCapacity); 12 } 13 // 指定長度的構造器,使用默認負載因子。 14 public HashMap(int initialCapacity) { 15 this(initialCapacity, DEFAULT_LOAD_FACTOR); 16 } 17 // 空參構造器,使用默認負載因子。 18 public HashMap() { 19 this.loadFactor = DEFAULT_LOAD_FACTOR; 20 } 21 // 傳入了一個集合的構造器,使用默認負載因子,添加指定集合。 22 public HashMap(Map<? extends K, ? extends V> m) { 23 this.loadFactor = DEFAULT_LOAD_FACTOR; 24 putMapEntries(m, false); 25 }
4.4 常用方法
1 // 獲取個數。 2 public int size() { 3 return size; 4 } 5 // 判斷是否為空。 6 public boolean isEmpty() { 7 return size == 0; 8 } 9 // 根據key獲取value,不存在會返回null。 10 public V get(Object key) { 11 Node<K,V> e; 12 return (e = getNode(hash(key), key)) == null ? null : e.value; 13 } 14 // 根據key獲取節點。 15 final Node<K,V> getNode(int hash, Object key) { 16 // 節點數組tab,數組首節點first,目標節點e,數組長度n,目標節點key值k。 17 Node<K,V>[] tab; Node<K,V> first, e; int n; K k; 18 // 賦值並判斷,如果數組已初始化,並且數組首節點不為空,才獲取元素。 19 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { 20 // 判斷數組首節點的key和value是否滿足,滿足則返回數組首節點。 21 if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) 22 return first; 23 // 數組首節點不滿足,賦值並遍歷鏈表和紅黑樹。 24 if ((e = first.next) != null) { 25 // 如果是紅黑樹節點,則通過紅黑樹節點方式查詢。 26 if (first instanceof TreeNode) 27 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 28 // 如果是鏈表節點,則遍歷鏈表節點查詢。 29 do { 30 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) 31 return e; 32 } while ((e = e.next) != null); 33 } 34 } 35 return null; 36 } 37 // 設置key和value鍵值對,返回原value,不存在會返回null。 38 public V put(K key, V value) { 39 return putVal(hash(key), key, value, false, true); 40 } 41 // 設置key和value鍵值對,返回原value,不存在會返回null。 42 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { 43 // 節點數組tab,指針節點p,數組長度n,數組位置i。 44 Node<K,V>[] tab; Node<K,V> p; int n, i; 45 // 賦值並判斷,如果數組未初始化,則初始化數組。 46 if ((tab = table) == null || (n = tab.length) == 0) 47 n = (tab = resize()).length 48 // 如果數組已初始化,並且指針節點不存在,則創建新節點存儲key和value。 49 if ((p = tab[i = (n - 1) & hash]) == null) 50 tab[i] = newNode(hash, key, value, null); 51 // 如果數組已初始化,並且指針節點存在,則查找key值相同節點並替換value。 52 else { 53 // 目標節點e,目標節點key值k。 54 Node<K,V> e; K k; 55 // 判斷指針節點的key和value是否滿足,滿足則將指針節點作為目標節點。 56 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) 57 e = p; 58 // 指針節點不滿足,並且是紅黑樹節點,遍歷紅黑樹並返回目標節點。 59 else if (p instanceof TreeNode) 60 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 61 // 指針節點不滿足,並且是鏈表節點,遍歷鏈表並返回目標節點。 62 else { 63 // 記錄鏈表節點個數並遍歷鏈表。 64 for (int binCount = 0; ; ++binCount) { 65 // 將下一節點作為目標節點,不存在則表示遍歷完成且不存在目標節點。 66 if ((e = p.next) == null) { 67 // 創建新節點存儲key和value。 68 p.next = newNode(hash, key, value, null); 69 // 如果新增后鏈表節點個數超過樹化閾值,則嘗試進行樹化操作。 70 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 71 treeifyBin(tab, hash); 72 // 此時目標節點不存在,跳出循環。 73 break; 74 } 75 // 遍歷過程中,找到滿足的目標節點。 76 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) 77 // 此時目標節點存在,跳出循環。 78 break; 79 // 將目標節點作為新的指針節點進入循環。 80 p = e; 81 } 82 } 83 // 如果目標節點存在,不需要個數自增和擴容,替換value並返回原值。 84 if (e != null) { 85 V oldValue = e.value; 86 if (!onlyIfAbsent || oldValue == null) 87 e.value = value; 88 afterNodeAccess(e); 89 return oldValue; 90 } 91 } 92 // 執行到這里,說明增加了新節點,操作數自增。 93 ++modCount; 94 // 個數自增,如果自增后的個數超過了閾值則進行擴容。 95 if (++size > threshold) 96 resize(); 97 // 添加新節點之后的后置處理。 98 afterNodeInsertion(evict); 99 // 返回null。 100 return null; 101 } 102 // 根據key刪除鍵值對,返回原value,不存在會返回null。 103 public V remove(Object key) { 104 Node<K,V> e; 105 return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value; 106 } 107 // 根據key刪除鍵值對,並返回原節點。 108 final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) { 109 // 節點數組tab,指針節點p,數組長度n,數組位置index。 110 Node<K,V>[] tab; Node<K,V> p; int n, index; 111 // 賦值並判斷,如果數組已初始化,並且數組首節點不為空,才刪除元素。 112 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { 113 // 原節點node,目標節點e,目標節點key值k,目標節點value值v。 114 Node<K,V> node = null, e; K k; V v; 115 // 判斷指針節點的key和value是否滿足,滿足則將指針節點作為原節點。 116 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) 117 node = p; 118 // 將指針節點下一節點作為目標節點,如果目標節點存在則繼續遍歷節點。 119 else if ((e = p.next) != null) { 120 // 如果指針節點是紅黑樹節點,則通過紅黑樹節點方式查詢原節點。 121 if (p instanceof TreeNode) 122 node = ((TreeNode<K,V>)p).getTreeNode(hash, key); 123 // 如果指針節點是鏈表節點,則通過鏈表節點方式查詢原節點。 124 else { 125 do { 126 // 遍歷過程中,找到滿足的目標節點。 127 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { 128 // 將目標節點作為原節點。 129 node = e; 130 // 此時原節點存在,跳出循環。 131 break; 132 } 133 // 用指針節點保存目標節點,跳出循環時,指針節點保存的是目標節點的上一節點。 134 p = e; 135 } while ((e = e.next) != null); 136 } 137 } 138 // 原節點存在,並且滿足value值的判斷規則,那就繼續執行。 139 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { 140 // 如果原節點是紅黑樹節點,則通過紅黑樹節點方式刪除原節點。 141 if (node instanceof TreeNode) 142 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); 143 // 如果原節點是鏈表節點,並且原節點和指針節點相同,則將原節點的下一節點作為數組首節點。 144 else if (node == p) 145 tab[index] = node.next; 146 // 如果原節點是鏈表節點,並且原節點和指針節點不同,則將原節點的下一節點作為指針節點的下一節點。 147 else 148 p.next = node.next; 149 // 執行到這里,說明刪除了原節點,操作數自增。 150 ++modCount; 151 // 個數自減。 152 --size; 153 afterNodeRemoval(node); 154 // 返回原節點。 155 return node; 156 } 157 } 158 return null; 159 } 160 // 清除所有元素。 161 public void clear() { 162 // 定義節點數組tab。 163 Node<K,V>[] tab; 164 // 操作數自增。 165 modCount++; 166 // 如果數組已初始化,則設置個數為0,並且將每個節點置空。 167 if ((tab = table) != null && size > 0) { 168 size = 0; 169 for (int i = 0; i < tab.length; ++i) 170 tab[i] = null; 171 } 172 }
4.5 擴容方法
1 final Node<K,V>[] resize() { 2 // 記錄原節點數組。 3 Node<K,V>[] oldTab = table; 4 // 記錄原數組容量。 5 int oldCap = (oldTab == null) ? 0 : oldTab.length; 6 // 記錄原閾值。 7 int oldThr = threshold; 8 // 定義新數組容量,定義新數組閾值。 9 int newCap, newThr = 0; 10 // 如果原容量大於0,則進行擴容。 11 if (oldCap > 0) { 12 // 如果原容量大於等於容量最大值。 13 if (oldCap >= MAXIMUM_CAPACITY) { 14 // 將原閾值設為整數最大值,並返回原數組。 15 threshold = Integer.MAX_VALUE; 16 return oldTab; 17 } 18 // 原容量擴容一倍並賦值給新容量,如果新容量小於容量最大值,並且原容量大於等於默認容量。 19 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) 20 // 將原閾值擴容一倍並賦值給新閾值。 21 newThr = oldThr << 1; 22 } 23 // 原容量為0,判斷原閾值是否大於0。 24 else if (oldThr > 0) 25 // 首次初始化,將原閾值賦值給新容量。 26 newCap = oldThr; 27 // 原容量為0,並且原閾值也是0。 28 else { 29 // 設置默認容量。 30 newCap = DEFAULT_INITIAL_CAPACITY; 31 // 設置默認閾值。 32 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 33 } 34 // 判斷新閾值是否為0。 35 if (newThr == 0) { 36 // 計算新閾值。 37 float ft = (float)newCap * loadFactor; 38 // 新容量小於容量最大值並且閾值小於容量最大值,則使用新閾值,否則使用最大值。 39 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); 40 } 41 // 確定新閥值。 42 threshold = newThr; 43 // 開始構造新的節點數組。 44 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 45 table = newTab; 46 // 如果原節點數組已初始化,則將原節點放置到新節點數組。 47 if (oldTab != null) { 48 // 遍歷原節點數組。 49 for (int j = 0; j < oldCap; ++j) { 50 // 定義原節點。 51 Node<K,V> e; 52 // 如果原節點不為空,記錄原節點並移動。 53 if ((e = oldTab[j]) != null) { 54 // 將原節點置空。 55 oldTab[j] = null; 56 // 如果原節點沒有子節點,表示數組中只有一個節點,直接放置到新節點數組。 57 if (e.next == null) 58 newTab[e.hash & (newCap - 1)] = e; 59 // 如果原節點是紅黑樹節點,則在紅黑樹中將原節點存儲到新節點數組。 60 else if (e instanceof TreeNode) 61 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 62 // 如果原節點是鏈表節點,則進行鏈表節點的移動。 63 else { 64 // 定義低位鏈表的頭節點和尾節點。 65 Node<K,V> loHead = null, loTail = null; 66 // 定義高位鏈表的頭節點和尾節點。 67 Node<K,V> hiHead = null, hiTail = null; 68 // 定義下一節點。 69 Node<K,V> next; 70 // 循環遍歷節點鏈表。 71 do { 72 // 給下一節點賦值。 73 next = e.next; 74 // 確定原節點在新節點數組中的位置,為0則放在低位鏈表。 75 if ((e.hash & oldCap) == 0) { 76 if (loTail == null) 77 loHead = e; 78 else 79 loTail.next = e; 80 loTail = e; 81 } 82 // 為1則放在高位鏈表。 83 else { 84 if (hiTail == null) 85 hiHead = e; 86 else 87 hiTail.next = e; 88 hiTail = e; 89 } 90 } while ((e = next) != null); 91 // 如果低位鏈表不為空,則將整個低位鏈表放到原位置。 92 if (loTail != null) { 93 loTail.next = null; 94 newTab[j] = loHead; 95 } 96 // 如果高位鏈表不為空,則將整個高位鏈表放到新增的空間中。 97 if (hiTail != null) { 98 hiTail.next = null; 99 newTab[j + oldCap] = hiHead; 100 } 101 } 102 } 103 } 104 } 105 // 返回新的節點數組。 106 return newTab; 107 }
5 補充說明
5.1 數組長度為2的倍數
長度減一后的二進制位全為1,可以用來計算節點在數組中的位置,不會造成浪費。
5.2 指定長度不為2的倍數
在構造方法中的最后一步會根據指定長度計算容量,會通過移位運算得到大於等於指定長度的,並且為2的倍數的最小正整數。
5.3 先使用hashCode()方法,再使用equals()方法
在Object類中有一個hashCode()方法,用來獲取對象的哈希值,也被稱作為散列值。
hashCode()方法被native修飾,意味着這個方法和平台有關。大多數情況下,hashCode()方法返回的是與對象信息(存儲地址和字段等)有關的數值。
當向集合中插入對象時,如果調用equals()逐個進行比較,雖然可行但是這樣做的效率很低。因此,先調用hashCode()進行判斷,如果相同再調用equals()判斷,就會提高效率。
5.4 使用hash()方法處理hashCode
在計算節點在數組中的下標時,一般是通過與節點有關的數值除以數組長度取余得到的。當數組長度為2的倍數時,取余操作相當於數值同數組長度減一進行與運算。
根據數組長度減一得到的結果,將二進制位分為高位和低位,將左邊全為0的部分作為高位,將右邊全為1的部分作為低位。在與運算時,任何數字與0相與只能得到0,所以高位無效,只用到了低位。
如果使用hashCode進行與運算,兩個hashCode不同但是低位相同的節點會被分到一個數組中,哈希碰撞發生的可能性較大。因此,需要對hashCode進行處理,使兩個高位不同低位相同的節點得到的結果也不同。
hash()方法被稱為擾動函數,是用來對hashCode進行處理的方法。在hash()方法處理后,hashCode高位的變化也會影響低位,這時再使用低位計算下標就能使元素的分布更加合理,哈希碰撞的可能性也會降低。
在JDK1.8版本中,只使用了hashCode進行了一次與運算:
1 static final int hash(Object key) { 2 int h; 3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 4 }
5.5 何時進行擴容
1)插入第一個節點時,初始化數組時進行擴容。
2)插入節點后,長度超過閾值時進行擴容。
5.6 擴容時對鏈表結構的處理
使用高低位鏈表,將節點的hash值同原數組長度進行與運算,根據結果0和1放到低位鏈表和高位鏈表。
將低位鏈表放置到新數組的低位,將高位鏈表放置到新數組的高位,低位的位置加上原數組長度就是高位的位置。
5.7 重寫equals方法和hashCode方法
一般在重寫equals()方法的時候,也會盡量重寫hashCode()方法,就是為了在equals()方法判斷相等的時候保證讓hashCode()方法判斷相等。
