(1). 前三個構造方法,無非就是設置一下初始化容量和負載因子,不多說; (2). 而第四個構造方法:
設置初始化容量和負載因子;inflateTable(…) 初始化數組并擴充容量;將 map 對象塞進空的 HashMap 中。(1). 一直往前推,很容易得知傳進來的 toSize 參數就是初始化容量; (2). roundUpToPowerOf2(toSize) 方法得到返回值是一個比 toSize 大的最小的 2 的 n 次方數 capacity,capacity 就是實際容量;
若 toSize 為 15,則 capacity 為 16;若 toSize 為 17,則 capacity 為 32。因此可以看出:table 數組的長度一定是 2 的 n 次方數;
(3). 計算得出,閥值 = 實際容量*負載因子(capacity*loadFactor)。 (4). new 出一個真實容量為 capacity 的 Entry 數組。
首先我們要明確一點,hash 算法有什么目的,說簡單了就是為了快。
hash 算法是為了減少查找過程中的比較次數;查找的最理想情況就是一次找到,當然這種情況往往是通過犧牲存儲空間實現的,實際應用之中并不可取,但是可以看出 hash 算法在查找方面的高效性。通常情況下,有 hash 函數的地方也能見到 indexFor 函數的身影。
static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1);}h & (length -1) 得到這個元素應該存儲在數組的哪個索引處。
Q:h & (length -1) 得到值永遠在數組的索引值范圍內,不會出現數組下標越界的情況,為什么呢 ? A:因為數組的長度是 2 的 n 次方數,所以 length -1 位最大的 n 位二進制數,即所有位都是 1:
當 h > length -1 時,h 的高位忽略,得到的數最大也就是 length -1;當 h <= length -1 時,若有高位則忽略所有高位,得到的數也不可能超過 length -1。Q:hash 函數并不是簡簡單單的求下哈希碼,而是在這個基礎上又做了多次位操作,這樣做有何目的呢 ? A:如果 hashCode 值都大于 length,而且這些 hashCode 的低位變化不大,就會出現很多沖突。舉個例子:
假設數組的初始化容量為 16(10000),則 length -1 位 15(1111);有幾個對象的 hashCode 分別為 1 1001 0010、1 1101 0010、11 1011 0010,進行位與運算的結果是一致的(都為 0010),即發生了沖突;而對 hashCode 進行位操作得到 1 1000 1000、1 1100 1100、11 1000 1110,進行位與運算分別得到 1000、1100、1110,減少了沖突。因此,發生沖突的原因是 16 限制了只能用低位來計算,高位直接被舍棄無法參與計算,所以需要進行位操作使高位對低位造成影響改變低位的值,從而變相地使高位也參與運算。
先看一下單個元素的 put 操作:
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordaccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null;}(1). 如果數組為空,先對其進行初始化并擴容; (2). 如果 key==null,使用 putForNullKey 操作(下面有代碼):
遍歷 table[0] 位置的鏈表,若遇到 key==null 的節點,就替換 value;若遇不到,就將這個元素插入鏈表的鏈首。由此可見,如果有key==null 的元素肯定存儲在table[0] 位置的鏈表中。
private V putForNullKey(V value) { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null;}(3). 如果 key不為 null,hash 之后進行定位得到數組下標 x,對 table[x] 遍歷所有進行比較,如果找得到就進行替換,找不到就插入鏈首。
再看下 putAll 操作:
public void putAll(Map<? extends K, ? extends V> m) { int numKeysToBeAdded = m.size(); if (numKeysToBeAdded == 0) return; if (table == EMPTY_TABLE) { inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold)); } if (numKeysToBeAdded > threshold) { int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1); if (targetCapacity > MAXIMUM_CAPACITY) targetCapacity = MAXIMUM_CAPACITY; int newCapacity = table.length; while (newCapacity < targetCapacity) newCapacity <<= 1; if (newCapacity > table.length) resize(newCapacity); } for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) put(e.getKey(), e.getValue());}(1). 若 map 長度為0,到此為止; (2). 若 table 為空,進行數組初始化并擴容; (3). 若 map 的元素個數大于閥值,進行擴容,擴充后的容量和目標容量進行比較,若還是如此繼續擴容,直到容量滿足條件為止(條件就是數組容量不小于目標容量),擴容為向左移一位變為兩倍。 (4). 剩下的就是遍歷 map 一個一個元素 put,不多說。
resize 會改變 HashMap 的存儲結構,對元素進行重新映射,是非常耗性能的操作。所以最好一開始就能確定新容量的取值(這一點在 putAll 中有很好的體現)
Q:HashMap 使用數組 + 鏈表的存儲結構,理論上可以無限存儲數據,為什么要對數組進行擴容呢? A:進行擴容能夠優化存儲結構,提高 HashMap 的性能;如果不進行擴容,鏈表長度就會慢慢變得很長,查找的時候很大概率遍歷到鏈表深處,就失去的 hash 查找的優勢。
先上代碼。
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; /** * 構造方法,創建一個 Entry 節點 */ Entry(int h, K k, V v, Entry<K,V> n) { // key,value,next 指針,hash 碼 value = v; next = n; key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return value; } // 設置新的 value,返回舊的 value public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } // 判斷兩個節點是否相等 public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } public final String toString() { return getKey() + "=" + getValue(); } /** * This method is invoked whenever the value in an entry is * overwritten by an invocation of put(k,v) for a key k that's already * in the HashMap. */ void recordAccess(HashMap<K,V> m) { } /** * This method is invoked whenever the entry is * removed from the table. */ void recordRemoval(HashMap<K,V> m) { }}Entry 實現了單向鏈表的功能。
所以,HashMap 的數據結構就是數組 + 鏈表:元素本身是存儲在 Entry 中的,這些 Entry 構成許多條鏈表,而所有鏈表的鏈首構成了 table 數組。數組的下標就是 indexFor 計算出的索引,但是不同元素會出現計算出索引相同的情況,這就是沖突了,解決這個沖突的辦法是具有相同索引的元素會以單向鏈表的方式存在。
下圖為 Hashmap 的存儲結構:

找到一個網站有助于理解 HashMap 的結構以及增刪改查操作:
https://www.cs.usfca.edu/~galles/visualization/OpenHash.html
與 ArrayList 等集合的快速失敗原理一樣。
先舉個例子:
public class Test { HashMap<Integer, Integer> map = new HashMap<>(); public static void main(String[] args) { Test test = new Test(); test.putAndPrint(); test.remove_1(); } public void remove_2() { try { // 暫停10秒。目的是保證迭代器正在迭代過程中。 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } Iterator<Integer> iterator = map.keySet().iterator(); iterator.remove(); } public void remove_1() { try { // 暫停10秒。目的是保證迭代器正在迭代過程中。 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } map.remove(1000); } public void putAndPrint() { T1 t1 = new T1(); Thread thread = new Thread(t1); thread.start(); } class T1 implements Runnable { @Override public void run() { for (int i = 0; i < 1000000; i++) { map.put(i, i); } Iterator<Integer> iterator = map.keySet().iterator(); for (;iterator.hasNext();) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } Integer integer = (Integer) iterator.next(); System.out.println(integer); } } }}輸出為:

我們可以分析一下,另起了一個線程向 map 中 put 1000000個元素(這個時間可以忽略),生成迭代器,每隔大約 0.1 秒輸出一個元素。 同時,主線程在 sleep 一秒鐘后對 map 進行了 remove 操作,此時迭代器正在迭代過程中。 map.remove 中有操作 modCount++,因此迭代器在 nextEntry 方法中發現 modCount != expectedModCount 為 false 拋出異常。
同理若把 main 方法中的test.remove_11(),改為test.remove_2(),也會拋出這個異常,因為 HashIterator 的 remove 方法同樣調的 map 的 remove 方法。

新聞熱點
疑難解答