版權聲明:本文出自汪磊的博客,未經作者允許禁止轉載。
存儲鍵值對我們首先想到HashMap,它的底層基於哈希表,采用數組存儲數據,使用鏈表來解決哈希碰撞,它是線程不安全的,並且存儲的key只能有一個為null,在安卓中如果數據量比較小(小於一千),建議使用SparseArray和ArrayMap,內存,查找性能方面會有提升,如果數據量比較大,幾萬,甚至幾十萬以上還是使用HashMap吧。本篇只詳細分析HashMap的源碼,SparseArray和ArrayMap不在本篇討論范圍內,后續會單獨分析。
HashMap的理解,最最核心就是擴容那二十幾行代碼,可以說是HashMap的核心所在了,然而網上絕大部分博客只是一帶而過,大體說了一下結論,讓人十分失望,本篇將會徹底分析擴容機制,源碼分析基於android-23。
好了,直入主題吧。
一、HashMap中成員變量
1 private static final int MINIMUM_CAPACITY = 4;//約定hashmap中最小容量,也可以是0,如果不為0,那么最小容量限制為4 2 private static final int MAXIMUM_CAPACITY = 1 << 30;約定hashmap中最大容量,為2的30次方
3 static final float DEFAULT_LOAD_FACTOR = .75F;//擴容因子:主要用於擴容時機,后續會細講
4
5 transient HashMapEntry<K, V>[] table;//盛放數據的table,數據每一項key不為null,每一項都是一個HashMapEntry對象
6
7 transient HashMapEntry<K, V> entryForNullKey;盛放key為null的數據項
8
9 transient int size;//hashmap中已經盛放數據的大小
10
11 private transient int threshold;//用來判斷是否需要擴容,其值為DEFAULT_LOAD_FACTOR * hashmap的容量,當盛放數據達到hashmap的四分之三時,急需要考慮擴容了
主要成員變量已經有所標注,后續分析的時候會再次提及,此處不做過多解釋。
二、HashMap中數據項HashMapEntry
HashMap中每個數據項都是HashMapEntry對象,HashMapEntry是HashMap的內部類,我們先來看看其結構:
1 static class HashMapEntry<K, V> implements Entry<K, V> { 2 final K key; 3 V value; 4 final int hash; 5 HashMapEntry<K, V> next; 6 7 HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) { 8 this.key = key; 9 this.value = value; 10 this.hash = hash; 11 this.next = next; 12 } 13 14 public final K getKey() { 15 return key; 16 } 17 18 public final V getValue() { 19 return value; 20 } 21 22 public final V setValue(V value) { 23 V oldValue = this.value; 24 this.value = value; 25 return oldValue; 26 } 27 28 @Override public final boolean equals(Object o) { 29 if (!(o instanceof Entry)) { 30 return false; 31 } 32 Entry<?, ?> e = (Entry<?, ?>) o; 33 return Objects.equal(e.getKey(), key) 34 && Objects.equal(e.getValue(), value); 35 } 36 37 @Override public final int hashCode() { 38 return (key == null ? 0 : key.hashCode()) ^ 39 (value == null ? 0 : value.hashCode()); 40 } 41 42 @Override public final String toString() { 43 return key + "=" + value; 44 } 45 }
主要信息就是每一個數據項都包含了我們存儲的key,value以及根據key算出來的hash值,next用於發生哈希碰撞的時候指向其下一個數據項。
三、HashMap構造方法
HashMap構造方法有如下:
1 public HashMap() 2 public HashMap(int capacity) 3 public HashMap(int capacity, float loadFactor) 4 public HashMap(Map<? extends K, ? extends V> map)
構造方法有如上4種方式,我們平時最常用的就是第一種方式,直接初始化然后不停往里面仍數據就可以了,第二種初始化的時候可以指定容量大小,第三,四中估計大部分人沒用過,第三種除了指定容量大小我們還可以指定擴容因子,不過我們還是不要動擴容因子為好,指定為0.75是時間和空間的權衡,平時我們使用就用默認的0.75就可以了。
我們看一下HashMap()這種構造方式:
1 public HashMap() { 2 table = (HashMapEntry<K, V>[]) EMPTY_TABLE; 3 threshold = -1; // Forces first put invocation to replace EMPTY_TABLE 4 }
太簡單了,就是初始化table為空的數組EMPTY_TABLE,這個EMPTY_TABLE的初始化容量可不為0,源碼如下:
private static final Entry[] EMPTY_TABLE = new HashMapEntry[MINIMUM_CAPACITY >>> 1];
看到了吧,初始化容量為MINIMUM_CAPACITY的一半,也就是2。
此外threshold初始化的時候置為-1。
接下來我們在看下HashMap(int capacity) 這種構造方式:
1 public HashMap(int capacity) { 2 if (capacity < 0) { 3 throw new IllegalArgumentException("Capacity: " + capacity); 4 } 5 6 if (capacity == 0) { 7 @SuppressWarnings("unchecked") 8 HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE; 9 table = tab; 10 threshold = -1; // Forces first put() to replace EMPTY_TABLE 11 return; 12 } 13 14 if (capacity < MINIMUM_CAPACITY) { 15 capacity = MINIMUM_CAPACITY; 16 } else if (capacity > MAXIMUM_CAPACITY) { 17 capacity = MAXIMUM_CAPACITY; 18 } else { 19 capacity = Collections.roundUpToPowerOfTwo(capacity); 20 } 21 makeTable(capacity); 22 } 23 24 25 private HashMapEntry<K, V>[] makeTable(int newCapacity) { 26 @SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable 27 = (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity]; 28 table = newTable; 29 threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity 30 return newTable; 31 }
6-12行邏輯大體就是我們調用hashmap()空參數的構造函數初始化一樣。
14-17行,就是對我們設置的容量capacity進行檢查了,如果小於MINIMUM_CAPACITY那么就重置為MINIMUM_CAPACITY,如果大於MAXIMUM_CAPACITY則重置為MAXIMUM_CAPACITY。
19行,Collections.roundUpToPowerOfTwo這個方法就是找出一個2^n的數,使其不小於給出的數字,並且最近接給出的數字。
比如:
Collections.roundUpToPowerOfTwo(3)返回4,22.
Collections.roundUpToPowerOfTwo(4)返回4,22.
Collections.roundUpToPowerOfTwo(100)返回128,27.
明白了吧?也就是說返回的數肯定是2的幾次方,也就是說hashmap的容量肯定是2的幾次方形式,這里很重要,一定要記住,后續分析的時候還會用到。
接下來就是調用makeTable了。
26,27就是根據給定的容量創建newTable數組。
28行,成員變量table指向新創建的newTable數組。
29行,計算threshold的值,也就是我們指定的容量的四分之三了。
好了,以上就是構造方法邏輯,其余兩種方法可自行查看一下,比較核心的就是19行代碼,對capacity數據的轉換,約束hashmap容量大小肯定為2的n次方。
四、HashMap中put方法分析
接下來就是HashMap核心所在了,我們一點點分析,先看下put方法源碼:
1 @Override 2 public V put(K key, V value) { 3 if (key == null) { 4 return putValueForNullKey(value); 5 } 6 int hash = Collections.secondaryHash(key); 7 HashMapEntry<K, V>[] tab = table; 8 int index = hash & (tab.length - 1); 9 for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) { 10 if (e.hash == hash && key.equals(e.key)) { 11 preModify(e); 12 V oldValue = e.value; 13 e.value = value; 14 return oldValue; 15 } 16 } 17 // No entry for (non-null) key is present; create one 18 modCount++; 19 if (size++ > threshold) { 20 tab = doubleCapacity(); 21 index = hash & (tab.length - 1); 22 } 23 addNewEntry(key, value, hash, index); 24 return null; 25 }
3-5行,如果我們放入的數據key為null,那么執行4行代碼邏輯並且直接返回,putValueForNullKey源碼如下:
1 private V putValueForNullKey(V value) { 2 HashMapEntry<K, V> entry = entryForNullKey; 3 if (entry == null) { 4 addNewEntryForNullKey(value); 5 size++; 6 modCount++; 7 return null; 8 } else { 9 preModify(entry); 10 V oldValue = entry.value; 11 entry.value = value; 12 return oldValue; 13 } 14 }
大體邏輯很簡單,就是對成員變量entryForNullKey操作,其就是HashMapEntry對象實例,3-8行如果entry為null,則代表之前沒有放入過key為null的數據,則只需要創建即可。8-12行表示之前放入鍋key為null的數據,那么只需要將value替換為新的value即可,這里說明HashMap只會有一個數據的key為null,重復放入只會將value替換為最新value.好了,這里就只是簡單分析一下。
回到put方法,如果我們放入的key不為null,則繼續向下執行:
6行,根據key計算二次哈希值,源碼如下:
1 public static int secondaryHash(Object key) { 2 return secondaryHash(key.hashCode()); 3 } 4 5 private static int secondaryHash(int h) { 6 // Spread bits to regularize both segment and index locations, 7 // using variant of single-word Wang/Jenkins hash. 8 h += (h << 15) ^ 0xffffcd7d; 9 h ^= (h >>> 10); 10 h += (h << 3); 11 h ^= (h >>> 6); 12 h += (h << 2) + (h << 14); 13 return h ^ (h >>> 16); 14 }
就是將key的hashCode方法返回的值傳入secondaryHash(int h) 再次計算一次返回一個值,這里最重要的一點就是我們傳入的key必須有hashCode()方法並且每次返回的值一樣,如果HashMap Key的哈希值在存儲鍵值對后發生改變,Map可能再也查找不到這個Entry了,所以HashMap中key我們需要使用不可變對象,比如經常使用的String,Integer對象,其HashCode()方法分別如下:
1 @Override 2 public int hashCode() {//String中HashCode()方法 3 int hash = hashCode; 4 if (hash == 0) { 5 if (count == 0) { 6 return 0; 7 } 8 for (int i = 0; i < count; ++i) { 9 hash = 31 * hash + charAt(i); 10 } 11 hashCode = hash; 12 } 13 return hash; 14 } 15 16 @Override 17 public int hashCode() {//Integer中HashCode()方法 18 return value; 19 }
回到put方法,則繼續向下執行:
7行定義局部變量tab指向全局變量table數組。
8行,計算放入的數據在tab中的位置,計算方式為key的hash值按位與tab的長度減1,這樣確保了計算出的index不會超出數組角標,比如:
key的hash值為11111111111111111111111111111111,tab容量為8,則tab.length-1為7,其數組角標范圍為0~7。
hash & (tab.length - 1)按位與計算如下:
也就是經過上述計算最大值為7,不會超過數組角標。
為什么要進行二次哈希值得計算呢?
比如我們放入三個數據,key的HashCode值分別為:31,63,95。tab容量為8
如果不進行二次哈希值計算索引index,也就是key.hashcode() & (tab.length - 1),計算如下:
31=00011111 & 00000111 = 0111 = 7
63=00111111 & 00000111 = 0111 = 7
95=01011111 & 00000111 = 0111 = 7
進行二次哈希值后再計算索引index,也就是源碼中secondaryHash(key.hashCode())& (tab.length - 1),計算如下:
31=00011111 =>secondaryHash=> 00011110 & 00000111= 0110 = 6
63=00111111 ==secondaryHash=> 00111100 & 00000111= 0100 = 4
95=01011111 ==secondaryHash=> 01011010 & 00000111= 0010 = 2
如上不經過二次哈希值計算最終計算出的index值均為7,也就是我們放入數組中都處於同一位置。而經過二次哈希值計算之后再計算index值分別為6,4,2也就是在數組中處於了三個不同的位置,這樣就達到了更加散列的效果。但是即使經過二次哈希值計算也不能保證計算出的index值都不相同,這里只是盡可能的散列化,不能保證避免哈希碰撞。
回到put方法,繼續向下分析:
我們知道HashMap存儲數據結構如下:
簡單說就是我們放入一個數據的時候會先根據數據項的key計算出其在table數組中的索引,如果索引位置已經有元素了,那么則與已經放入的數據形成鏈表的關系,相信稍有經驗的都明白,這里只是稍微提一下。
9-16行的for循環邏輯就是挨個遍歷所在數組行鏈表中每一個數據項,然后將每個數據項的hash值和key與將要放入的key及其hash值比較,如果二者均相等則表明HashMap中已經存在此數據。
12-14行就是將對應數據項的value值替換為新的value值,並將之前value返回。
如果整個for循環都沒有找到則表明HashMap中沒有將要存儲的數據項,繼續向下執行。
19行,判斷是否需要擴容,threshold上面說過值為table容量的四分之三,size記錄我們HashMap中存入數據的大小,我們放入數據時如果超過容量的四分之三那么就需要擴容了。
20行,如果需要擴容那么調用doubleCapacity()方法進行擴容(后續會仔細分析擴容機制),擴容完此方法會返回擴容后的數組。
21行,由於數組已經擴容,容量發生了變化,所以這里需要重新計算一下將要放入數據的index索引。
23行調用addNewEntry方法將數據放入數組中。addNewEntry源碼如下:
1 void addNewEntry(K key, V value, int hash, int index) { 2 table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]); 3 }
這里就是根據我們傳入的key,value,hash值新建HashMapEntry數據節點,此數據節點的next指向原table[index],最后將新數據節點賦值給table[index],這里說的有點蒙圈,用圖來解釋一下,又要展示我強大的畫圖能力了:
這里通過閱讀源碼可以發現新添加的數據項是放在鏈表頭部的,而不是直接放在尾部。
好了,以上就是put方法主要邏輯了,不再做其余分析,下面我們着重看一下HashMap的擴容機制。
五、HashMap中擴容機制分析
好了,如果你看到這里那么清理一下大腦吧,下面的有點燒腦了。
廢話少說,直接看擴容方法源碼;
1 private HashMapEntry<K, V>[] doubleCapacity() { 2 HashMapEntry<K, V>[] oldTable = table; 3 int oldCapacity = oldTable.length; 4 if (oldCapacity == MAXIMUM_CAPACITY) { 5 return oldTable; 6 } 7 int newCapacity = oldCapacity * 2; 8 HashMapEntry<K, V>[] newTable = makeTable(newCapacity); 9 if (size == 0) { 10 return newTable; 11 } 12 for (int j = 0; j < oldCapacity; j++) { 13 /* 14 * Rehash the bucket using the minimum number of field writes. 15 * This is the most subtle and delicate code in the class. 16 */ 17 HashMapEntry<K, V> e = oldTable[j]; 18 if (e == null) { 19 continue; 20 } 21 int highBit = e.hash & oldCapacity; 22 HashMapEntry<K, V> broken = null; 23 newTable[j | highBit] = e; 24 for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) { 25 int nextHighBit = n.hash & oldCapacity; 26 if (nextHighBit != highBit) { 27 if (broken == null) 28 newTable[j | nextHighBit] = n; 29 else 30 broken.next = n; 31 broken = e; 32 highBit = nextHighBit; 33 } 34 } 35 if (broken != null) 36 broken.next = null; 37 } 38 return newTable; 39 }
2-6行oldTable,oldCapacity記錄原來數組,數組長度以及檢查原數組長度是否已經達到MAXIMUM_CAPACITY,如果已經達到最大長度,那么不好意思了,直接返回原數組了,老子無法給你擴容了,都那么長了,還擴什么容,自己繼續在原數組玩吧,管你哈希碰撞導致鏈表多長我都不管了。
7行,定義newCapacity也就是新數組長度為原數組長度的2倍。
8行,就是執行makeTable()邏輯,創建新的數組newTable了,至於makeTable方法上面說過,就不再分析了。
9-11行,檢查size是否為0,如果為0那么表明HashMap中沒有存儲過數據,不用執行下面的數據拷貝邏輯了,直接返回newTable就可以了。
12-37行,這可就是HashMap整個類的精華所在了,這幾行代碼看不懂這個類你就沒有真正理解,看懂了其余掃一下就明白了。
假設原HashMap如圖:
12行很簡單就是遍歷原數組中每個位置的數據,也可以說每個鏈表的頭數據。
13-16行,風騷的注釋:直白翻譯就是下面這幾行代碼是這個類中最風騷的幾行代碼。
17-20行代碼,就是檢查取出的數組中每個數據項是否為null,為null則表明此行沒有數據,繼續循環就可以了。
在繼續向下講請大家思考一個問題:HashMap中同一個鏈表中每一個數據項的哈希值有什么相同點?比如原數組大小是8,那么同一鏈表中每一個數據項的哈希值有什么相同點?
思考。。。。。
這里直接說了:能在同一鏈表說明計算出來的index值相同,在看計算公式為int index = hash & (tab.length - 1),這里在擴容之前tab.length-1的值是相同的,比如數組長度為8,那么tab.length - 1的二進制表示為00000111,不同hash值計算出的index又相同,那么這里同一鏈表中每一個數據項的hash值得最后三位一定相同,只有這樣計算出的index值才相同,如下:
如上圖 兩個hash值不同的數據項,經過運算后得出index均為2,原因就是雖然整體的hash值不同,但是最后三位均為010,所以計算出index值是相同的(此處假設數組長度為8)。
進而得出結論:如果HashMap中數組長度為2的n次方,那么同一鏈表中不同數據項的hash值的最后n位一定相同。
好了,到這里第一個難點通過,我們繼續分析doubleCapacity()方法。
21行,int highBit = e.hash & oldCapacity計算出highBit位,翻譯過來就是高位,這他媽又是什么玩意?仔細看計算方式與的oldCapacity,而不是oldCapacity-1,所以這里取得是數據項hash值得第n+1位(hashmap數組長度為2的n次方)是0還是1,這里一定要知道HashMap數組長度一定為2的n次方,二進制形式就是第n+1位為1其余為均為0。這里先記住這個highBit是哪一位,后面會用到。
22行,定義一個broken,知道有這么個玩意,后面也會用到。
23行,newTable[j | highBit] = e,將我們從原數組取出的數據項放入新數組中,也就是數據的拷貝了,注意這里e是每一個鏈表的頭部,也就是處於數組中的數據。鏈表其余數據是通過24行for循環挨個遍歷再放入新數組中的。但是這里有個疑問原數組放入數據是按照hash & (tab.length - 1)計算其在數組中位置的,這里怎么成了j | highBit這樣計算了呢?這里真是卡住我了,一開始我是怎么想怎么想不通,但是我覺得二者之間一定有什么關系,不可能用兩個完全不相關的算法來計算同一數據項在數組中的位置,絕不可能,一定有內在聯系,我查啊查,算啊算,在經過如下計算我終於想明白了:hash & (tab.length - 1)與j | highBit這二者邏輯是完全相同的,TMD,邏輯竟然是相同的。
接下來咱們推導分析一下:
j | highBit
= j | (e.hash & oldCapacity) 第一步
= (e.hash & (oldCapacity-1)) | (e.hash & oldCapacity) 第二步
= e.hash & ( (oldCapacity-1) | oldCapacity) 第三步
= e.hash & (newCapacity- 1) 第四步
從開始到第一步很簡單了,highBit的計算方式就是e.hash & oldCapacity這里只是替換回來。
第一步到第二步,j怎么就成了e.hash & (oldCapacity-1)呢?還記得index的計算方式嗎?就是e.hash & (oldCapacity-1),那就是說j就是index了,在看看j是什么?j就是從0開始到oldCapacity的值,這里我們想一下啊,e就是通過oldTable[j]獲取的,我們想想put方法怎么放入的呢,不就是oldTable[e.hash & (oldCapacity-1)] = e嗎,想到了什么?想到了什么?對,通過j獲取的元素e,這個j就是e.hash & (oldCapacity-1),所以這里可以替換的。
第二步到第三步就是數學方面的,記住就可以了。
第三步到第四步怎么來的呢?也就是(oldCapacity-1) | oldCapacity與newCapacity- 1相等,還記得上面說的HashMap數組容量一定是2的n次方嗎?並且newCapacity = oldCapacity * 2 。
oldCapacity為2的n次方,也就是n+1位為1,其余都為0,oldCapacity-1也就是0到n位為1其余都為0,二者或運算后0到n+1為1其余位0。
newCapacity= oldCapacity * 2 也就是n+2位為1其余位0,newCapacity- 1也就是0到n+1位為1其余為0.
所以(oldCapacity-1) | oldCapacity與newCapacity- 1相等。
到此,我們就證明了j | highBit = e.hash & (newCapacity- 1) 其計算數據項在新數組中位置與原數組的計算邏輯是一樣的,只不過十分巧妙的運用了位運算,好了,想明白這里恭喜你通過了第二個難點,我們繼續向下分析。
24-34行就是遍歷鏈表中數據項了,把他們挨個放入新數組中,這里思考一個問題?在原數組中同一鏈表的數據項在新數組中還處於同一鏈表嗎?如果不是那么是什么決定它們不在同一鏈表了?
在上面分析的時候我們得出一個結論:如果HashMap中數組長度為2的n次方,那么同一鏈表中不同數據項的hash值的最后n位一定相同。
擴容后數組容量為原來的2倍了,根據index的計算方式e.hash & (newCapacity- 1)每個數據項的hash值是不變的,但是長度變了,所以同一鏈表中不同數據項在新數組中不一定還處於同一鏈表,那么具體是什么決定在新數組中二者在不在同一鏈表呢?
原數組長度為2的n次方,新數組長度擴容后為原數組2倍也就是2的n+1次方,原數組中同一鏈表中不同數據項的hash值的最后n位一定相同,所以新數組同一鏈表中不同數據項的hash值的最后n+1位一定相同。如果上面講的你真的理解了,這里就不難理解,不過多解釋。
在原數組中同一鏈表的數據項已經確保了hash值最后n位相同,按照計算方式新數組中處於同一鏈表的數據項需要確保hash值最后n+1位相同即可,既然原鏈表中的數據項最后n位已經相同了,在新數組中是否處於同一鏈表那么只需要比較同鏈表數據項hash值的第n+1位即可,如果相同則表明在新數組中依然處於同一鏈表,如果不同那么就處於不同鏈表了,上面的高位highBit就是取的是每一數據項的第n+1位,后面比較也只是比較每個數據項的highBit是否相同。
好了,這里我認為解釋的已經很清楚了,這里你要是明白了,恭喜你,HashMap中最難理解的部分你已經完全掌握了。
至於24-34行具體邏輯我就不一一分析了,靜下心來,自己試着分析,難度不大。
六、總結
好了,到這里本篇就要結束了,咦?這不就分析了一個put方法外加擴容機制嗎?這就完了?是的,我想說的就這些,這部分是最難理解的,至於其余自己看看都能理解的差不多了。
最關鍵是一定要理解擴容機制,那幾行最難理解的代碼設計的真是巧妙,大神的思想真是無法企及啊!!!
本篇到此結束,希望對你有用。
聲明:文章將會陸續搬遷到個人公眾號,以后文章也會第一時間發布到個人公眾號,及時獲取文章內容請關注公眾號