HashMap是基於哈希表的Map接口實現,此實現提供全部可選的映射操做,並容許使用null值和null鍵。HashMap與HashTable的做用大體相同,可是它不是線程安全的。此類不保證映射的順序,特別是它不保證該順序恆久不變。node
遍歷HashMap的時間複雜度與其的容量(capacity)和現有元素的個數(size)成正比。若是要保證遍歷的高效性,初始容量(capacity)不能設置過高或者平衡因子(load factor)不能設置過低。算法
1.HashMap是存儲鍵值對(key,value)的一種數據結構。
2.每個元素都是一個key-value。
3.HashMap最多隻容許一個key爲null,容許多個key的value值爲null。
4.HashMap是非線程安全的,只適用於單線程環境。
5.HashMap實現了Serializable、Cloneable接口,所以它支持序列化和克隆。數組
JDK1.7的HashMap是基於一個數組加多個單鏈表來實現的,hash值衝突時,就將對應節點以鏈表的形式存儲,這樣子HashMap在性能上就存在必定的問題,爲何這麼說呢?安全
由於若是成百上千個節點在hash時發生碰撞,那麼若是要查找其中一個節點,最差的狀況下要查找的節點就是鏈表末尾的節點,那麼最差狀況下的時間複雜度爲 O(n) ,這樣毫無疑問會形成性能低下。數據結構
所以這問題在JDK1.8中獲得了很好解決的方案,在JDK1.8中採用的是位桶+鏈表/紅黑樹的結構實現,而在JDK1.8中的時候鏈表長度達到一個闕值(一般節點數量 > 8 )的時候就會轉換成紅黑樹結構,至於紅黑樹,本身去了解後再來看此篇文章,衆所周知紅黑樹的時間複雜度爲 log n ,這無疑是對性能的一次大提高。相對於JDK1.7的位桶+鏈表的實現方式來講,性能誰優誰劣,可想而知。併發
接下來從底層結構、put和get方法、hash數組索引、擴容機制等幾個方面來分析HashMap的實現原理:函數
首先看一下JDK1.7中HashMap的底層結構圖,以下所示:性能
接下來,讓咱們先看看JDK1.7中HashMap類中的成員變量,以下:優化
/** 初始容量,默認16 */ (1) static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** 最大初始容量,2^30 */ (2) static final int MAXIMUM_CAPACITY = 1 << 30; /** 負載因子,默認0.75,負載因子越小,hash衝突機率越低 */ (3) static final float DEFAULT_LOAD_FACTOR = 0.75f; /** 初始化一個Entry的空數組 */ (4) static final Entry<?,?>[] EMPTY_TABLE = {}; /** 將初始化好的空數組賦值給table,table數組是HashMap實際存儲數據的地方,並不在EMPTY_TABLE數組中 */ (5) transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; /** HashMap實際存儲的元素個數 */ (6) transient int size; /** 臨界值(HashMap 實際能存儲的大小),公式爲(threshold = capacity * loadFactor) */ (7) int threshold; /** 負載因子 */ (8) final float loadFactor; /** HashMap的結構被修改的次數,用於迭代器 */ (9) transient int modCount;
代碼(1)初始化桶大小,由於底層是數組,因此這是數組默認的大小。即16。this
代碼(2)桶最大值。即2的30次方
代碼(3)默認的負載因子(0.75),負載因子越小,hash衝突機率越低 。
代碼(4)將初始化好的空數組賦值給table,table數組是HashMap實際存儲數據的地方,並不在EMPTY_TABLE數組中 。HashMap內部的存儲結構是一個數組,此處數組爲空,即沒有初始化以前的狀態
代碼(5)table
真正存放數據的數組。
代碼(6)Map
存放數量的大小。HashMap實際存儲的元素個數。實際存儲的key-value鍵值對的個數
代碼(7)桶大小,可在初始化時顯式指定。當table == {}時,該值爲初始容量(初始容量默認爲16);當table被填充了,也就是爲table分配內存空間後,threshold通常爲 capacity*loadFactory。HashMap在進行擴容時須要參考threshold。
代碼(8)負載因子,可在初始化時顯式指定。表明了table的填充度有多少
代碼(9)HashMap的結構被修改的次數,用於迭代器。用於快速失敗,因爲HashMap非線程安全,在對HashMap進行迭代時,若是期間其餘線程的參與致使HashMap的結構發生變化了(好比put,remove等操做),須要拋出異常ConcurrentModificationException
接下來,讓咱們看看HashMap的構造函數,以下所示:
//計算Hash值時的key transient int hashSeed = 0; //經過初始容量和狀態因子構造HashMap public HashMap(int initialCapacity, float loadFactor) {
//(1) if (initialCapacity < 0)//參數有效性檢查 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
//(2) if (initialCapacity > MAXIMUM_CAPACITY)//參數有效性檢查 initialCapacity = MAXIMUM_CAPACITY;
//(3) if (loadFactor <= 0 || Float.isNaN(loadFactor))//參數有效性檢查 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity;
//(4) init(); } //(5) public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //(6) public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); } //(7) public HashMap(Map<? extends K, ? extends V> m) { this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1, DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
//(8) inflateTable(threshold);
//(9) putAllForCreate(m); }
代碼(1)校驗初始化容量大小。非法參數則拋異常
代碼(2)初始化容量是否大於容量的最大值,若是大於,將初始化容量設置成最大值。
代碼(3)校驗加載因子,不合法的加載因子則拋異常。
代碼(4)init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
代碼(5)經過擴容因子構造HashMap,容量取默認值,即16 。
代碼(6)裝載因子取0.75,容量取16,構造HashMap 。
代碼(7)經過其餘Map來初始化HashMap,容量經過其餘Map的size來計算,裝載因子取0.75 。
代碼(8)初始化HashMap底層的數組結構。
代碼(9)添加m中的元素。
給定的默認容量爲 16,負載因子爲 0.75。Map 在使用過程當中不斷的往裏面存放數據,當數量達到了 16 * 0.75 = 12
就須要將當前 16 的容量進行擴容,而擴容這個過程涉及到 rehash、複製數據等操做,因此很是消耗性能。
所以一般建議能提早預估 HashMap 的大小最好,儘可能的減小擴容帶來的性能損耗。
根據代碼能夠看到其實真正存放數據的是
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
這個數組,那麼它又是如何定義的呢?
// 靜態內部類 static class Entry<K,V> implements Map.Entry<K,V> {
//(1) final K key;
//(2) V value;
//(3) Entry<K,V> next; // 只想下一個entry節點
//(4) int hash; /** * 構造函數,每次都用新的節點指向鏈表的頭結點。新節點做爲鏈表新的頭結點 */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; // !!! key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return 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 (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); } //(5) void recordAccess(HashMap<K,V> m) { } //(6) void recordRemoval(HashMap<K,V> m) { } }
代碼(1)key 就是寫入時的鍵。
代碼(2)value 天然就是值。
代碼(3)開始的時候就提到 HashMap 是由數組和鏈表組成,因此這個 next 就是用於實現鏈表結構。
代碼(4)hash 存放的是當前 key 的 hashcode。
代碼(5)每當Entry中的值被已在HashMap中的鍵k的put(k,v)調用覆蓋時,都會調用此方法。
代碼(6)每當從表中刪除Entry時,都會調用此方法。
瞭解了基本結構,那來看看其中重要的put 方法和get方法:
public V put(K key, V value) {
//(1) if (table == EMPTY_TABLE) { inflateTable(threshold); }
//(2) if (key == null) return putForNullKey(value);
//(3) int hash = hash(key);
//(4) int i = indexFor(hash, table.length);
//(5) for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k;
//(6) if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } //(7) modCount++;
//(8) addEntry(hash, key, value, i); return null; }
代碼(1)若是table數組爲空數組{},進行數組填充(爲table分配實際內存空間),入參爲threshold,此時threshold爲initialCapacity 默認是1<<4(=16)。、
代碼(2)若「key爲null」,則將該鍵值對添加到table[0]處,遍歷該鏈表,若是有存在key爲null的entry,則將value替換。沒有就建立新Entry對象放在鏈表表頭,因此table[0]的位置上,永遠最多存儲1個Entry對象,造成不了鏈表。key爲null的Entry存在這裏。
代碼(3)若key不爲null,則計算該key的hash值,而後將其添加到該哈希值對應的數組索引處的鏈表中。
代碼(4)根據hash值計算桶號。
代碼(5)遍歷該桶中的鏈表。
代碼(6)若是其hash值相等且鍵也相等,將新值替換舊值,並返回舊值。
代碼(7)保證併發訪問時,若HashMap內部結構發生變化,快速響應失敗。即修改次數+1
代碼(8)若是桶是空的,說明當前位置沒有數據存入;新增一個 Entry 對象寫入當前位置。
接下來咱們進入到代碼(1)中的數組空間分配的方法 inflateTable(threshold),源碼以下:
private void inflateTable(int toSize) { int capacity = roundUpToPowerOf2(toSize);//(1) threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//(2) table = new Entry[capacity];//(3) initHashSeedAsNeeded(capacity);//(4) }
代碼(1)capacity必定是2的次冪
代碼(2)此處爲threshold賦值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy必定不會超過MAXIMUM_CAPACITY,除非loadFactor大於1。
代碼(3)分配空間。
代碼(4)選擇合適的Hash因子。
inflateTable這個方法用於爲主幹數組table在內存中分配存儲空間,經過roundUpToPowerOf2(toSize)能夠確保capacity爲大於或等於toSize的最接近toSize的二次冪,好比toSize=13,則capacity=16;to_size=16,capacity=16;to_size=17,capacity=32。roundUpToPowerOf2(toSize)源碼以下所示:
private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; }
roundUpToPowerOf2中的這段處理使得數組長度必定爲2的次冪,Integer.highestOneBit是用來獲取最左邊的bit(其餘bit位爲0)所表明的數值。
接下來咱們看看put方法中的hash方法的計算,源碼以下:
//(1) final int hash(Object k) {
//(2) int h = hashSeed;
//(3) if (0 != h && k instanceof String) {//這裏針對String優化了Hash函數,是否使用新的Hash函數和Hash因子有關 return sun.misc.Hashing.stringHash32((String) k); } //(4) h ^= k.hashCode(); //(5) h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
h ^= k.hashCode(): 0101 1101 0010 1111 1100 0110 0011 0101 ------------------------------------------------------------ h >>> 20: : 0000 0000 0000 0000 0000 0101 1101 0010 h >>> 12 : 0000 0000 0000 0101 1101 0010 1111 1100 ------------------------------------------------------------ (h >>> 20) ^ (h >>> 12) : 0000 0000 0000 0101 1101 0111 0010 1110 ----------------------------------------------------------- h ^= (h >>> 20) ^ (h >>> 12) : 0101 1101 0010 1010 0001 0001 0001 1011 ----------------------------------------------------------- (h >>> 7) : 0000 0000 1011 1010 0101 0100 0010 0010 (h >>> 4) : 0000 0101 1101 0010 1010 0001 0001 0001 ----------------------------------------------------------- (h >>> 7) ^ (h >>> 4) :0000 0101 0110 1000 1111 0101 0011 0011 ----------------------------------------------------------- h ^ (h >>> 7) ^ (h >>> 4) :0101 1000 0100 0010 1110 0100 0010 1000 ----------------------------------------------------------- h & (length-1) :0000 0000 0000 0000 0000 0000 0000 1000 = 8
代碼(1)判斷k的數據類型選擇不一樣的hash計算方式。用了不少的異或,移位等運算,對key的hashcode進一步進行計算以及二進制位的調整等來保證最終獲取的存儲位置儘可能分佈均勻。
代碼(2)隨機種子,用來下降衝突發生的概率
代碼(3)這裏針對String優化了Hash函數,是否使用新的Hash函數和Hash因子有關。
代碼(5)這個函數確保哈希碼在每一個位的倍數不變的狀況下只會發生有限數量的碰撞(默認負載係數大約爲8)。
從上面的操做看以看出,影響HashMap元素的存儲位置的只有key的值,與value值無關。
就這樣,經過高低位之間進行異或用來加大低位的隨機性,以減小衝突的概率。
經過hash函數獲得散列值後,再經過indexFor進一步處理來獲取實際的存儲位置,其實現以下:
//返回數組下標 static int indexFor(int h, int length) {
//(1) return h & (length-1); }
代碼(1)把hash值和數組的長度進行「與」操做。
該方法用於肯定元素存放於數組的位置,可是參數h是一個由hash方法計算而來的int類型數據,若是直接拿h做爲下標訪問HashMap主數組的話,考慮到2進制32位帶符號的int值範圍從-2147483648到2147483648,該值可能會很大,因此這個值不能直接使用,要用它對數組的長度進行取模運算,獲得的餘數才能用來當作數組的下標,這就是indexFor方法作的事情。(由於length老是爲2的N次方,因此h & (length-1)操做等價於hash % length操做, 但&操做性能更優)。
該方法也是HashMap的數組長度爲何老是2的N次方的緣由。2的N次方 - 1的二進制碼是一個「低位掩碼」,「與」操做後會把hash值的高位置零,只保留低位的值,使用這種方法使值縮小。以初始長度16爲例,16-1=15。2進製表示是00000000 00000000 00001111。和某散列值作「與」操做以下,結果就是截取了最低的四位值。例子以下:
10100101 11000100 00100101 & 00000000 00000000 00001111 ---------------------------------- 00000000 00000000 00000101 //高位所有歸零,只保留末四位
這樣,就算差距很大的兩個數,只要低位相同,那麼就會產生衝突,會對性能形成很大的影響,因而,hash方法的做用就體現出來了。
接着咱們再看看普通方法中調用的addEntry方法,以下:
void addEntry(int hash, K key, V value, int bucketIndex) {
//(1) if ((size >= threshold) && (null != table[bucketIndex])) {
//(2) resize(2 * table.length);
//(3) hash = (null != key) ? hash(key) : 0;
//(4) bucketIndex = indexFor(hash, table.length); } //(5) createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) {
//(6) Entry<K,V> e = table[bucketIndex];
//(7) table[bucketIndex] = new Entry<>(hash, key, value, e);
//元素個數加1 size++; }
代碼(1)當調用 addEntry 寫入 Entry 時須要判斷是否須要擴容。
代碼(2)當size超過臨界閾值threshold,而且即將發生哈希衝突時進行擴容,新容量爲舊容量的2倍。
代碼(3)擴容後,從新計算哈希值。
代碼(4)擴容後從新計算插入的位置下標,即從新計算桶號。
代碼(5)createEntry
中會將當前位置的桶傳入到新建的桶中,若是當前桶有值就會在位置造成鏈表。
代碼(6)獲取待插入位置元素
代碼(7)這裏執行連接操做,使得新插入的元素指向原有元素。這保證了新插入的元素老是在鏈表的頭。
發生哈希衝突而且size大於閾值的時候,須要進行數組擴容,擴容時,須要新建一個長度爲以前數組2倍的新的數組,而後將當前的Entry數組中的元素所有傳輸過去,擴容後的新數組長度爲以前的2倍,因此擴容相對來講是個耗資源的操做。接下來讓咱們看看resize(2 * table.length)的擴容方法,源碼以下:
//按新的容量擴容Hash表
void resize(int newCapacity) {
//(1)
Entry[] oldTable = table;
//(2)
int oldCapacity = oldTable.length;
//(3)
if (oldCapacity == MAXIMUM_CAPACITY) {
//(4)
threshold = Integer.MAX_VALUE;//修改擴容閥值
return;
}
//(5)
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
//(6)計算是否須要對鍵從新進行哈希碼的計算
useAltHashing |= sun.misc.VM.isBooted() && (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
//(7)
transfer(newTable, rehash);
//(8)
table = newTable;
//(9)
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
代碼(1)先獲取老的數據
代碼(2)獲取老的容量值。
代碼(3)若是老的容量值已經到了最大容量值,則修改擴容闕值
代碼(5)建立新的結構
代碼(6)計算是否須要對鍵從新進行哈希碼的計算
代碼(7)將老的表中的數據拷貝到新的結構中。將原有全部的桶遷移至新的桶數組中 ,在遷移時,桶在桶數組中的絕對位置可能會發生變化 *,這就是爲何HashMap不能保證存儲條目的順序不能恆久不變的緣由
代碼(8)修改HashMap的底層數組
代碼(9)修改閥值
接下來讓咱們進入到tranfer方法中,看看是如何進行桶遷移至新的桶數組中的,源碼以下:
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //(1) for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next;
//(2) if (rehash) { //(3) e.hash = null == e.key ? 0 : hash(e.key); } //(4) int i = indexFor(e.hash, newCapacity); //(5) e.next = newTable[i]; //(6) newTable[i] = e;
//(7) e = next; } } }
代碼(1)遍歷當前的table,將裏面的元素添加到新的newTable中。
代碼(2)若是是從新Hash
代碼(3)從新計算hash值。
代碼(4)計算桶號
代碼(5)元素鏈接到桶中,這裏至關於單鏈表的插入,老是插入在最前面
代碼(6)存放在數組下標i中,因此擴容後鏈表的順序與原來相反。
代碼(7)繼續下一個元素。
接着,讓咱們看看HashMap的put方法,源碼以下:
//(1)
public V get(Object key) {
//(2) if (key == null) return getForNullKey();
//(3) Entry<K,V> entry = getEntry(key); //(4) return null == entry ? null : entry.getValue(); } final Entry<K,V> getEntry(Object key) {
//(5) if (size == 0) { return null; } //(6) int hash = (key == null) ? 0 : hash(key);
//(7) for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k;
//(8) if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; }
//(9) return null; }
//獲取key爲null的實體 private V getForNullKey() { if (size == 0) {//若是元素個數爲0,則直接返回null return null; } //key爲null的元素存儲在table的第0個位置 for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null)//判斷是否爲null return e.value;//返回其值 } return null; }
代碼(1)獲取key值爲key的元素值。
代碼(2)若是Key值爲空,則獲取對應的值,這裏也能夠看到,HashMap容許null的key,其內部針對null的key有特殊的邏輯。
代碼(3)若是建不爲null,獲取Entry實體。
代碼(4)判斷是否爲空,不爲空,則獲取對應的值。
代碼(5)元素個數爲 0 ,直接返回null。
代碼(6)計算key的hash值。
代碼(7)根據key和表的長度,定位到Hash桶。
代碼(8)遍歷直到 key 及 hashcode 相等時候就返回值。
代碼(9)啥也沒取到直返回null。
接着讓咱們看最後一個HashMap的方法,remove方法,源碼以下:
final Entry<K,V> removeEntryForKey(Object key) { //計算鍵的hash值 int hash = (key == null) ? 0 : hash(key); //計算桶號 int i = indexFor(hash, table.length); //記錄待刪除節點的上一個節點 Entry<K,V> prev = table[i]; //待刪除節點 Entry<K,V> e = prev; while (e != null) { Entry<K,V> next = e.next; Object k; //是不是將要刪除的節點 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; //將要刪除的節點是否爲鏈表的頭部 if (prev == e) //鏈表的頭部指向下一節點 table[i] = next; else //上一節點的NEXT爲將要刪除節點的下一節點 prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
到目前爲止,不知道你們有沒有發現JDK1.7中須要優化的地方?
正如開篇提到的當 Hash 衝突嚴重時,在桶上造成的鏈表會變的愈來愈長,這樣在查詢時的效率就會愈來愈低;時間複雜度爲 O(N)
。
所以JDK1.8中中採用的是位桶+鏈表/紅黑樹的結構實現,而在JDK1.8中的時候鏈表長度達到一個闕值(一般節點數量 > 8 )的時候就會轉換成紅黑樹結構,至於紅黑樹,本身去了解後再來看此篇文章,衆所周知紅黑樹的時間複雜度爲 log n ,這無疑是對性能的一次大提高。相對於JDK1.7的位桶+鏈表的實現方式來講,性能誰優誰劣,可想而知。
JDK1.8中的HashMap的結構圖以下:
接着讓咱們看看JDK1.8中的HashMap的成員變量。源碼以下:
//(1) 默認的初始容量是16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //(2) 桶最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //(3) 默認的負載因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //(4) 當桶(bucket)上的結點數大於這個值時會轉成紅黑樹 static final int TREEIFY_THRESHOLD = 8; //(5) 當桶(bucket)上的結點數小於這個值時樹轉鏈表 static final int UNTREEIFY_THRESHOLD = 6; //(6) 桶中結構轉化爲紅黑樹對應的table的最小大小 static final int MIN_TREEIFY_CAPACITY = 64; //(7) 存儲元素的數組,老是2的冪次倍 transient Node<k,v>[] table; //(8) 存放具體元素的集 transient Set<map.entry<k,v>> entrySet; //(9) 存放元素的個數,注意這個不等於數組的長度。 transient int size; //(10) 每次擴容和更改map結構的計數器 transient int modCount; //(11) 臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴容 int threshold; //(12) 負載因子 final float loadFactor;
代碼(1)初始化桶大小,由於底層是數組,因此這是數組默認的大小。即16。
代碼(2)桶最大值。即2的30次方
代碼(3)默認的負載因子(0.75),負載因子越小,hash衝突機率越低 。
代碼(4)用於判斷是否須要將鏈表轉換爲紅黑樹的閾值。當桶(bucket)上的結點數大於這個值時會轉成紅黑樹
代碼(5)當桶(bucket)上的結點數小於這個值時樹轉鏈表
代碼(6)桶中結構轉化爲紅黑樹對應的table的最小大小
代碼(7)存儲元素的數組,老是2的冪次倍。當table == {}時,該值爲初始容量(初始容量默認爲16);當table被填充了,也就是爲table分配內存空間後,threshold通常爲 capacity*loadFactory。HashMap在進行擴容時須要參考threshold。
代碼(8)存放具體元素的集。
代碼(9)存放元素的個數,注意這個不等於數組的長度。
代碼(10)HashMap的結構被修改的次數,用於迭代器。用於快速失敗,因爲HashMap非線程安全,在對HashMap進行迭代時,若是期間其餘線程的參與致使HashMap的結構發生變化了(好比put,remove等操做),須要拋出異常ConcurrentModificationException。
代碼(11)臨界值 當實際大小(容量*填充因子)超過臨界值時,會進行擴容。、
代碼(12)負載因子。
能夠看到JDK1.8中HashMap的成員變量和 1.7 大致上都差很少。
構造函數稍微有點變化,JDK1.8的構造函數源碼以下:
public HashMap(int initialCapacity, float loadFactor) { // 桶初始容量不能小於0,不然報錯 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 桶初始容量不能大於最大值,不然爲最大值 if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //校驗負載因子 負載因子不能小於或等於0,不能爲非數字 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 初始化負載因子 this.loadFactor = loadFactor; // 初始化threshold大小 this.threshold = tableSizeFor(initialCapacity); } public HashMap(int initialCapacity) { // 調用HashMap(int, float)型構造函數,經過擴容因子構造HashMap,容量取默認值,即16 。 this(initialCapacity, DEFAULT_LOAD_FACTOR); } public HashMap() { // 初始化負載因子,裝載因子取0.75,容量取16,構造HashMap this.loadFactor = DEFAULT_LOAD_FACTOR; } //經過其餘Map來初始化HashMap,容量經過其餘Map的size來計算,裝載因子取0.75 public HashMap(Map<? extends K, ? extends V> m) { // 初始化負載因子 this.loadFactor = DEFAULT_LOAD_FACTOR; // 將m中的全部元素添加至HashMap中 putMapEntries(m, false); }
不一樣在於:
1.初始化threshold大小 this.threshold = tableSizeFor(initialCapacity);有所不一樣,tableSizeFor(initialCapacity)返回大於initialCapacity的最小的二次冪數值。源碼以下:
static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
>>> 操做符表示無符號右移,高位取0。
2.經過其餘Map來初始化HashMap,putMapEntries(Map<? extends K, ? extends V> m, boolean evict)函數將m的全部元素存入本HashMap實例中。源碼以下:
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { // 判斷table是否已經初始化 if (table == null) { // pre-size // 未初始化,s爲m的實際元素個數 float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); // 計算獲得的t大於閾值,則初始化閾值 if (t > threshold) threshold = tableSizeFor(t); } // 已初始化,而且m元素個數大於閾值,進行擴容處理 else if (s > threshold) resize(); // 將m中的全部元素添加至HashMap中 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }
接着咱們再看JDK1.8中的Hash算法,源碼以下:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
首先獲取對象的hashCode()值,而後將hashCode值右移16位,而後將右移後的值與原來的hashCode作異或運算,返回結果。(其中h>>>16,在JDK1.8中,優化了高位運算的算法,使用了零擴展,不管正數仍是負數,都在高位插入0)。
接着再看看JDK1.8中的put方法和get方法中的變化。
先看put方法,以下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // (1) if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // (2) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已經存在元素 else { Node<K,V> e; K k; // (3) if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) // 將第一個元素賦值給e,用e來記錄 e = p; // (4) else if (p instanceof TreeNode) // 放入樹中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 該鏈爲鏈表 // 爲鏈表結點 else { //(5) for (int binCount = 0; ; ++binCount) { // 到達鏈表的尾部 if ((e = p.next) == null) { // 在尾部插入新結點 p.next = newNode(hash, key, value, null); // (6) if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 跳出循環 break; } // (7) if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 相等,跳出循環 break; // 用於遍歷桶中的鏈表,與前面的e = p.next組合,能夠遍歷鏈表 p = e; } } // (8) if (e != null) { // 記錄e的value V oldValue = e.value; // onlyIfAbsent爲false或者舊值爲null if (!onlyIfAbsent || oldValue == null) //用新值替換舊值 e.value = value; // 訪問後回調 afterNodeAccess(e); // 返回舊值 return oldValue; } } // 結構性修改 ++modCount; // (9) if (++size > threshold) resize(); // 插入後回調 afterNodeInsertion(evict); return null; }
看似要比 1.7 的複雜,咱們一步步拆解:
代碼(1)判斷當前桶是否爲空,空的就須要初始化(resize 中會判斷是否進行初始化)。
代碼(2)根據當前 key 的 hashcode 定位到具體的桶中並判斷是否爲空,爲空代表沒有 Hash 衝突就直接在當前位置建立一個新桶便可。
代碼(3)若是當前桶有值( Hash 衝突),那麼就要比較當前桶中的 key、key 的 hashcode
與寫入的 key 是否相等,相等就賦值給 e
,在第 8 步的時候會統一進行賦值及返回。
代碼(4)若是當前桶爲紅黑樹,那就要按照紅黑樹的方式寫入數據。
代碼(5)若是是個鏈表,就須要將當前的 key、value 封裝成一個新節點寫入到當前桶的後面(造成鏈表)。
代碼(6)接着判斷當前鏈表的大小是否大於預設的閾值,大於時就要轉換爲紅黑樹。
代碼(7)判斷鏈表中結點的key值與插入的元素的key值是否相等
代碼(8)接着判斷當前鏈表的大小是否大於預設的閾值,大於時就要轉換爲紅黑樹。
代碼(9)最後判斷是否須要進行擴容。超過最大容量就擴容,實際大小大於閾值則擴容。
HashMap的數據存儲實現原理
流程:
1. 根據key計算獲得key.hash = (h = k.hashCode()) ^ (h >>> 16);
2. 根據key.hash計算獲得桶數組的索引index = key.hash & (table.length - 1),這樣就找到該key的存放位置了:
① 若是該位置沒有數據,用該數據新生成一個節點保存新數據,返回null;
② 若是該位置有數據是一個紅黑樹,那麼執行相應的插入 / 更新操做;
③ 若是該位置有數據是一個鏈表,分兩種狀況一是該鏈表沒有這個節點,另外一個是該鏈表上有這個節點,注意這裏判斷的依據是key.hash是否同樣:
若是該鏈表沒有這個節點,那麼採用尾插法新增節點保存新數據,返回null;若是該鏈表已經有這個節點了,那麼找到該節點並更新新數據,返回老數據。
注意:
HashMap的put會返回key的上一次保存的數據,好比:
HashMap<String, String> map = new HashMap<String, String>();
System.out.println(map.put("a", "A")); // 打印null
System.out.println(map.put("a", "AA")); // 打印A
System.out.println(map.put("a", "AB")); // 打印AA
接着看紅黑樹如何插入數據的, putTreeVal(this, tab, hash, key, value) 源碼以下:
/** * Tree version of putVal. * 紅黑樹插入會同時維護原來的鏈表屬性, 即原來的next屬性 */ final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab, int h, K k, V v) { Class<?> kc = null; boolean searched = false; // 查找根節點, 索引位置的頭節點並不必定爲紅黑樹的根結點 TreeNode<K,V> root = (parent != null) ? root() : this; for (TreeNode<K,V> p = root;;) { // 將根節點賦值給p, 開始遍歷 int dir, ph; K pk; if ((ph = p.hash) > h) // 若是傳入的hash值小於p節點的hash值 dir = -1; // 則將dir賦值爲-1, 表明向p的左邊查找樹 else if (ph < h) // 若是傳入的hash值大於p節點的hash值, dir = 1; // 則將dir賦值爲1, 表明向p的右邊查找樹 // 若是傳入的hash值和key值等於p節點的hash值和key值, 則p節點即爲目標節點, 返回p節點 else if ((pk = p.key) == k || (k != null && k.equals(pk))) return p; // 若是k所屬的類沒有實現Comparable接口 或者 k和p節點的key相等 else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) { if (!searched) { // 第一次符合條件, 該方法只有第一次才執行 TreeNode<K,V> q, ch; searched = true; // 從p節點的左節點和右節點分別調用find方法進行查找, 若是查找到目標節點則返回 if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null) || ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null)) return q; } // 不然使用定義的一套規則來比較k和p節點的key的大小, 用來決定向左仍是向右查找 dir = tieBreakOrder(k, pk); // dir<0則表明k<pk,則向p左邊查找;反之亦然 } TreeNode<K,V> xp = p; // xp賦值爲x的父節點,中間變量,用於下面給x的父節點賦值 // dir<=0則向p左邊查找,不然向p右邊查找,若是爲null,則表明該位置即爲x的目標位置 if ((p = (dir <= 0) ? p.left : p.right) == null) { // 走進來表明已經找到x的位置,只需將x放到該位置便可 Node<K,V> xpn = xp.next; // xp的next節點 // 建立新的節點, 其中x的next節點爲xpn, 即將x節點插入xp與xpn之間 TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn); if (dir <= 0) // 若是時dir <= 0, 則表明x節點爲xp的左節點 xp.left = x; else // 若是時dir> 0, 則表明x節點爲xp的右節點 xp.right = x; xp.next = x; // 將xp的next節點設置爲x x.parent = x.prev = xp; // 將x的parent和prev節點設置爲xp // 若是xpn不爲空,則將xpn的prev節點設置爲x節點,與上文的x節點的next節點對應 if (xpn != null) ((TreeNode<K,V>)xpn).prev = x; moveRootToFront(tab, balanceInsertion(root, x)); // 進行紅黑樹的插入平衡調整 return null; } } }
1.查找當前紅黑樹的根結點,將根結點賦值給p節點,開始進行查找
2.若是傳入的hash值小於p節點的hash值,將dir賦值爲-1,表明向p的左邊查找樹
3.若是傳入的hash值大於p節點的hash值, 將dir賦值爲1,表明向p的右邊查找樹
4.若是傳入的hash值等於p節點的hash值,而且傳入的key值跟p節點的key值相等, 則該p節點即爲目標節點,返回p節點
5.若是k所屬的類沒有實現Comparable接口,或者k和p節點的key使用compareTo方法比較相等:第一次會從p節點的左節點和右節點分別調用find方法(見上文代碼塊2)進行查找,若是查找到目標節點則返回;若是不是第一次或者調用find方法沒有找到目標節點,則調用tieBreakOrder方法(見下文代碼塊5)比較k和p節點的key值的大小,以決定向樹的左節點仍是右節點查找。
6.若是dir <= 0則向左節點查找(p賦值爲p.left,並進行下一次循環),不然向右節點查找,若是已經沒法繼續查找(p賦值後爲null),則表明該位置即爲x的目標位置,另外變量xp用來記錄查找的最後一個節點,即下文新增的x節點的父節點。
7.以傳入的hash、key、value參數和xp節點的next節點爲參數,構建x節點(注意:xp節點在此處多是葉子節點、沒有左節點的節點、沒有右節點的節點三種狀況,即便它是葉子節點,它也可能有next節點,紅黑樹的結構跟鏈表的結構是互不影響的,不會由於某個節點是葉子節點就說它沒有next節點,紅黑樹在進行操做時會同時維護紅黑樹結構和鏈表結構,next屬性就是用來維護鏈表結構的),根據dir的值決定x決定放在xp節點的左節點仍是右節點,將xp的next節點設爲x,將x的parent和prev節點設爲xp,若是原xp的next節點(xpn)不爲空, 則將該節點的prev節點設置爲x節點, 與上面的將x節點的next節點設置爲xpn對應。
8.進行紅黑樹的插入平衡調整,
接下來讓咱們看看鏈表如何轉紅黑樹,treeifyBin(tab, hash),源碼以下:
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // table爲空或者table的長度小於64, 進行擴容 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); // 根據hash值計算索引值, 遍歷該索引位置的鏈表 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); // 鏈表節點轉紅黑樹節點 if (tl == null) // tl爲空表明爲第一次循環 hd = p; // 頭結點 else { p.prev = tl; // 當前節點的prev屬性設爲上一個節點 tl.next = p; // 上一個節點的next屬性設置爲當前節點 } tl = p; // tl賦值爲p, 在下一次循環中做爲上一個節點 } while ((e = e.next) != null); // e指向下一個節點 // 將table該索引位置賦值爲新轉的TreeNode的頭節點 if ((tab[index] = hd) != null) hd.treeify(tab); // 以頭結點爲根結點, 構建紅黑樹 }
1.校驗table是否爲空,若是長度小於64,則調用resize方法(見下文resize方法)進行擴容。
2.根據hash值計算索引值,將該索引位置的節點賦值給e節點,從e節點開始遍歷該索引位置的鏈表。
3.調用replacementTreeNode方法(該方法就一行代碼,直接返回一個新建的TreeNode)將鏈表節點轉爲紅黑樹節點,將頭結點賦值給hd節點,每次遍歷結束將p節點賦值給tl,用於在下一次循環中做爲上一個節點進行一些鏈表的關聯操做(p.prev = tl 和 tl.next = p)。
4.將table該索引位置賦值爲新轉的TreeNode的頭節點hd,若是該節點不爲空,則以hd爲根結點,調用treeify方法(見下文代碼塊7)構建紅黑樹。
接着看如何構建紅黑樹treeify(Node<K,V>[] tab),源碼以下:
final void treeify(Node<K,V>[] tab) { // 構建紅黑樹 TreeNode<K,V> root = null; for (TreeNode<K,V> x = this, next; x != null; x = next) {// this即爲調用此方法的TreeNode next = (TreeNode<K,V>)x.next; // next賦值爲x的下個節點 x.left = x.right = null; // 將x的左右節點設置爲空 if (root == null) { // 若是尚未根結點, 則將x設置爲根結點 x.parent = null; // 根結點沒有父節點 x.red = false; // 根結點必須爲黑色 root = x; // 將x設置爲根結點 } else { K k = x.key; // k賦值爲x的key int h = x.hash; // h賦值爲x的hash值 Class<?> kc = null; // 若是當前節點x不是根結點, 則從根節點開始查找屬於該節點的位置 for (TreeNode<K,V> p = root;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) // 若是x節點的hash值小於p節點的hash值 dir = -1; // 則將dir賦值爲-1, 表明向p的左邊查找 else if (ph < h) // 與上面相反, 若是x節點的hash值大於p節點的hash值 dir = 1; // 則將dir賦值爲1, 表明向p的右邊查找 // 走到這表明x的hash值和p的hash值相等,則比較key值 else if ((kc == null && // 若是k沒有實現Comparable接口 或者 x節點的key和p節點的key相等 (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) // 使用定義的一套規則來比較x節點和p節點的大小,用來決定向左仍是向右查找 dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; // xp賦值爲x的父節點,中間變量用於下面給x的父節點賦值 // dir<=0則向p左邊查找,不然向p右邊查找,若是爲null,則表明該位置即爲x的目標位置 if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; // x的父節點即爲最後一次遍歷的p節點 if (dir <= 0) // 若是時dir <= 0, 則表明x節點爲父節點的左節點 xp.left = x; else // 若是時dir > 0, 則表明x節點爲父節點的右節點 xp.right = x; // 進行紅黑樹的插入平衡(經過左旋、右旋和改變節點顏色來保證當前樹符合紅黑樹的要求) root = balanceInsertion(root, x); break; } } } } moveRootToFront(tab, root); // 若是root節點不在table索引位置的頭結點, 則將其調整爲頭結點 }
1.從調用此方法的節點做爲起點,開始進行遍歷,並將此節點設爲root節點,標記爲黑色(x.red = false)。
2.若是當前節點不是根結點,則從根節點開始查找屬於該節點的位置(該段代碼跟以前的代碼塊2和代碼塊4的查找代碼相似)。
3.若是x節點(將要插入紅黑樹的節點)的hash值小於p節點(當前遍歷到的紅黑樹節點)的hash值,則向p節點的左邊查找。
4.與3相反,若是x節點的hash值大於p節點的hash值,則向p節點的右邊查找。
5.若是x的key沒有實現Comparable接口,或者x節點的key和p節點的key相等,使用tieBreakOrder方法(見上文代碼塊5)來比較x節點和p節點的大小,以決定向左仍是向右查找(dir <= 0向左,不然向右)。
6.若是dir <= 0則向左節點查找(p賦值爲p.left,並進行下一次循環),不然向右節點查找,若是已經沒法繼續查找(p賦值後爲null),則表明該位置即爲x的目標位置,另外變量xp用來記錄最後一個節點,即爲下文新增的x節點的父節點。
7.將x的父節點設置爲xp,根據dir的值決定x決定放在xp節點的左節點仍是右節點,最後進行紅黑樹的插入平衡調整。
8.調用moveRootToFront方法(以下:)將root節點調整到索引位置的頭結點。
/** * 若是當前索引位置的頭節點不是root節點, 則將root的上一個節點和下一個節點進行關聯, * 將root放到頭節點的位置, 原頭節點放在root的next節點上 */ static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) { int n; if (root != null && tab != null && (n = tab.length) > 0) { int index = (n - 1) & root.hash; TreeNode<K,V> first = (TreeNode<K,V>)tab[index]; if (root != first) { // 若是root節點不是該索引位置的頭節點 Node<K,V> rn; tab[index] = root; // 將該索引位置的頭節點賦值爲root節點 TreeNode<K,V> rp = root.prev; // root節點的上一個節點 // 若是root節點的下一個節點不爲空, // 則將root節點的下一個節點的prev屬性設置爲root節點的上一個節點 if ((rn = root.next) != null) ((TreeNode<K,V>)rn).prev = rp; // 若是root節點的上一個節點不爲空, // 則將root節點的上一個節點的next屬性設置爲root節點的下一個節點 if (rp != null) rp.next = rn; if (first != null) // 若是原頭節點不爲空, 則將原頭節點的prev屬性設置爲root節點 first.prev = root; root.next = first; // 將root節點的next屬性設置爲原頭節點 root.prev = null; } assert checkInvariants(root); // 檢查樹是否正常 }
1.校驗root是否爲空、table是否爲空、table的length是否大於0。
2.根據root節點的hash值計算出索引位置,判斷該索引位置的頭節點是否爲root節點,若是不是則進行如下操做將該索引位置的頭結點替換爲root節點。
3.將該索引位置的頭結點賦值爲root節點,若是root節點的next節點不爲空,則將root節點的next節點的prev屬性設置爲root節點的prev節點。
4.若是root節點的prev節點不爲空,則將root節點的prev節點的next屬性設置爲root節點的next節點(3和4兩個操做是一個完整的鏈表移除某個節點過程)。
5.若是原頭節點不爲空,則將原頭節點的prev屬性設置爲root節點
6.將root節點的next屬性設置爲原頭節點(5和6兩個操做將first節點接到root節點後面)
7.root此時已經被放到該位置的頭結點位置,所以將prev屬性設爲空。
8.調用checkInvariants方法 檢查樹是否正常。
接着咱們在看看JDK1.8中的get方法,源碼以下:
public V get(Object key) { Node<k,v> e;
//(1) return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; //(2) if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // (3) if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; //(4) 桶中不止一個結點 if ((e = first.next) != null) { //(5) 爲紅黑樹結點 if (first instanceof TreeNode) // 在紅黑樹中查找 return ((TreeNode<K,V>)first).getTreeNode(hash, key); //(6) 不然,在鏈表中查找 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
get 方法看起來就要簡單許多了。
代碼(1)首先將 key hash 以後取得所定位的桶。
代碼(2)若是桶爲空則直接返回 null 。
代碼(3)不然判斷桶的第一個位置(有多是鏈表、紅黑樹)的 key 是否爲查詢的 key,是就直接返回 value。
代碼(4)若是第一個不匹配,則判斷它的下一個是紅黑樹仍是鏈表。
代碼(5)紅黑樹就按照樹的查找方式返回值。
代碼(6)否則就按照鏈表的方式遍歷匹配返回值。
最後咱們看看JDK1.8中的resize()擴容方法,源碼以下:
①.在jdk1.8中,resize方法是在hashmap中的鍵值對大於閥值時或者初始化時,就調用resize方法進行擴容;
②.每次擴展的時候,都是擴展2倍;
③.擴展後Node對象的位置要麼在原位置,要麼移動到原偏移量兩倍的位置。
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table;//oldTab指向hash桶數組 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) {//若是oldCap不爲空的話,就是hash桶數組不爲空 if (oldCap >= MAXIMUM_CAPACITY) {//若是大於最大容量了,就賦值爲整數最大的閥值 threshold = Integer.MAX_VALUE; return oldTab;//返回 }//若是當前hash桶數組的長度在擴容後仍然小於最大容量 而且oldCap大於默認值16 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold 雙倍擴容閥值threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; 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; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶數組 table = newTab;//將新數組的值複製給舊的hash桶數組 if (oldTab != null) {//進行擴容操做,複製Node對象值到新的hash桶數組,注意這個地方,這裏併發可能會形成數據丟失 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) {//若是舊的hash桶數組在j結點處不爲空,複製給e oldTab[j] = null;//將舊的hash桶數組在j結點處設置爲空,方便gc if (e.next == null)//若是e後面沒有Node結點 newTab[e.hash & (newCap - 1)] = e;//直接對e的hash值對新的數組長度求模得到存儲位置 else if (e instanceof TreeNode)//若是e是紅黑樹的類型,那麼添加到紅黑樹中 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 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;//將Node結點的next賦值給next if ((e.hash & oldCap) == 0) {//若是結點e的hash值與原hash桶數組的長度做與運算爲0 if (loTail == null)//若是loTail爲null loHead = e;//將e結點賦值給loHead else loTail.next = e;//不然將e賦值給loTail.next loTail = e;//而後將e複製給loTail } else {//若是結點e的hash值與原hash桶數組的長度做與運算不爲0 if (hiTail == null)//若是hiTail爲null hiHead = e;//將e賦值給hiHead else hiTail.next = e;//若是hiTail不爲空,將e複製給hiTail.next hiTail = e;//將e複製個hiTail } } while ((e = next) != null);//直到e爲空 if (loTail != null) {//若是loTail不爲空 loTail.next = null;//將loTail.next設置爲空 newTab[j] = loHead;//將loHead賦值給新的hash桶數組[j]處 } if (hiTail != null) {//若是hiTail不爲空 hiTail.next = null;//將hiTail.next賦值爲空 newTab[j + oldCap] = hiHead;//將hiHead賦值給新的hash桶數組[j+舊hash桶數組長度] } } } } } return newTab; }
從這兩個核心方法(get/put)能夠看出 1.8 中對大鏈表作了優化,修改成紅黑樹以後查詢效率直接提升到了 O(logn)
。
可是 HashMap 原有的問題也都存在,好比在併發場景下使用時容易出現死循環(JDK1.8中不會出現死循環了,可是併發可能會出現數據丟失,主要由於擴容的時候複製Node對象值到新的hash桶數組
會可能出現數據丟失)。以下:
咱們再回頭看一下咱們的 transfer代碼中的這個細節:
while(null != e) { Entry<K,V> next = e.next;// <--假設線程一執行到這裏就被調度掛起了 if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; }
線程一剛執行上面第一行代碼被調度掛起,線程二執行完成了。Jdk 1.8之前,致使死循環的主要緣由是擴容後,節點的順序會反掉,以下圖:擴容前節點A在節點C前面,而擴容後節點C在節點A前面。因而有以下的圖:
由於Thread1的 e 指向了key(3),而next指向了key(7),其在線程二rehash後,指向了線程二重組後的鏈表。咱們能夠看到鏈表的順序被反轉後。
接着線程一調度回來繼續執行。
1.先是執行 newTalbe[i] = e;
2.而後是e = next,致使了e指向了key(7),
3.而下一次循環的next = e.next致使了next指向了key(3)
線程一接着工做。把key(7)摘下來,放到newTable[i]的第一個,而後把e和next往下移。
環形連接出現。
e.next = newTable[i] 致使 key(3).next 指向了 key(7)
注意:此時的key(7).next 已經指向了key(3), 環形鏈表就這樣出現了。