. 前言
HashMap的容量大小會根據其存儲數據的數量多少而自動擴充,即當HashMap存儲數據的數量到達一個閾值(threshold)時,再往里面增加數據,便可能會擴充HashMap的容量。
可能?
事實上,由於JDK版本的不同,其閾值(threshold)的默認大小也變得不同(主要是計算公式的改變),甚至連判斷條件也變得不一樣,所以如果說threshold = capacity * loadFactor(容量 * 負載因子)將不再絕對正確,甚至說超過閾值容量就會增長也不再絕對正確,下面就以JDK1.6、1.7、1.8中的源碼說明。
注:本文無圖,標題僅是為了與前一篇文字標題符合
2. JDK 1.6
JDK 1.6中HashMap構造函數源碼如下(其中以Mark開頭注釋以及中文注釋,非JDK源碼中注釋,下同):
public HashMap(int initialCapacity, float loadFactor) { // Mark A Begin if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // Find a power of 2 >= initialCapacity int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; // Mark A End this.loadFactor = loadFactor; threshold = (int)(capacity * loadFactor); // 計算閾值,重點在這句代碼 table = new Entry[capacity]; init(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
其中,代碼片段A(Mark A Begin - Mark A End,下同)處可以先忽略,主要的是
threshold = (int)(capacity * loadFactor);
- 1
這邊是閾值的計算公式,其中capacity(容量) 的缺省值為16,loadFactor(負載因子)缺省值為0.75,那么
threshold = (int)(16 * 0.75) = 12
- 1
再來看addEntry函數(put(K, V)方法最后通過此函數插入數據,具體參見【圖解JDK源碼】HashMap的基本原理與它的線程安全性):
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); if (size++ >= threshold) // 判斷是否擴充容量 resize(2 * table.length); }
- 1
- 2
- 3
- 4
- 5
- 6
可以看見,只要當前數量大於或等於閾值,便會擴充HashMap的容量為其當前容量的2倍。這是在JDK 1.6下的特性。
3. JDK 1.7
JDK1.7中HashMap構造函數源碼如下:
public HashMap(int initialCapacity, float loadFactor) { // Mark A Begin if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // Mark A End this.loadFactor = loadFactor; threshold = initialCapacity; // 計算閾值,重點在這句代碼 init(); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
同樣的,代碼片段A可以先忽略,那么對么上面代碼,可以看出,閾值的計算與JDK 1.6中完全不同,它與合約因子無關,而是直接使用了初始大小作為閾值的大小,但是這僅是針對第一次改變大小前,因為在resize函數(改變容量大小的函數,擴充容量便是調用此函數)中,有如下代碼:
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
- 1
也即是說,在改變一次大小后,threshold的值仍然跟負載因子相關,與JDK 1.6中的計算方式相差無幾(未討論容量到達最大值1,073,741,824 時的情況)。
而addEntry函數也與JDK 1.6中有所不同,其源碼如下:
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { // 判斷語句發生了改變 resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
從上面的代碼可以看出,在JDK 1.6中,判斷是否擴充大小是直接判斷當前數量是否大於或等於閾值,而JDK 1.7中可以看出,其判斷是否要擴充大小除了判斷當前數量是否大於等於閾值,同時也必須保證當前數據要插入的桶不能為空(桶的詳細可參見【圖解JDK源碼】HashMap的基本原理與它的線程安全性)。那么JDK 1.8中又是如何呢?
3. JDK 1.8
說明:JDK 1.8對於HashMap的實現,新增了紅黑樹的特點,所以其底層實現原理變得不一樣,再此不討論。
JDK 1.8中HashMap構造函數源碼如下:
public HashMap(int initialCapacity, float loadFactor) { // Mark A Begin if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // Mark A End this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); // 計算閾值 }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
這里使用到了tableSizeFor方法,其源碼如下:
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; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
因為使用了位運算,所以這個方法可能不能明確的知道結果,但是只要知道不管輸入什么值,它的最后結果都會是0,1,2,4,8,16,32,68… 這些數字中的一個就對了(其實是有規律的),對於以下輸入值有:
tableSizeFor(16) = 16 tableSizeFor(32) = 32 tableSizeFor(48) = 64 tableSizeFor(64) = 64 tableSizeFor(80) = 128 tableSizeFor(96) = 128 tableSizeFor(112) = 128 tableSizeFor(128) = 128 tableSizeFor(144) = 256
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
也即是說,對於容量的初始值16來說,其初始閾值便是16,與JDK 1.7中初始閾值相同,而其resize函數中,threshold的計算源碼如下:
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double 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; // 代碼太多,省略后面的代碼 }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
計算變得更復雜,因為其底層實現原理已經不僅僅是像之前的JDK中數組加鏈表那樣簡單,但是仍然可以看見其閾值的計算是與負載因子相關的。
而其判斷是否要擴充的語句在putVal函數內(put方法會調用),其源碼如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // 代碼太多,省略 ++modCount; if (++size > threshold) // 判斷是否擴充語句 resize(); afterNodeInsertion(evict); return null; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
可以看見,其判斷是否達到閾值與JDK 1.6是相同的,而沒有像JDK 1.7中那樣判斷桶是否不為空。
總結
JDK 1.6 當數量大於容量 * 負載因子即會擴充容量。
JDK 1.7 初次擴充為:當數量大於容量時擴充;第二次及以后為:當數量大於容量 * 負載因子時擴充。
JDK 1.8 初次擴充為:與負載因子無關;第二次及以后為:與負載因子有關。其詳細計算過程需要具體詳解。
注:以上均未考慮最大容量時的情況。
