距離寫第一篇關於
HashMap
的筆記(CSDN鏈接、HEXO鏈接)已經有1年多了,感覺當年的分析中還是少寫了些內容,現在看起來有些部分有點費勁。所以現在特地補充上現在的分析筆記,主要集中在resize()
擴容這一個操作上面。
從put()
方法進入,經putVal()
到達resize()
,開始進行擴容過程。大致分爲2個過程,即確定新的容量及臨界容量、擴容後的調整。
確定新的容量及臨界容量
對這一部分代碼的總體理解如下:
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) { // 此時Map不爲空,裏面存在鍵值對
if (oldCap >= MAXIMUM_CAPACITY) { // oldCap >= 0x3fff ffff
threshold = Integer.MAX_VALUE; // threshold = 0x7fff ffff
return oldTab;
} // newCap = oldCap * 2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
else if (oldThr > 0) // 此時Map爲空,但是new對象的時候,把capacity當做參數,
newCap = oldThr; // 傳給了HashMap的構造函數,造成了只有threshold有值。
else { // 此時Map爲空,使用的是無參構造器,threshold和capacity都沒有值。
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// newThr在程序開始處的默認賦值爲0
if (newThr == 0) { // 此處補充上述沒有爲newThr賦值的情況,即
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 使用newCap、newThr初始化擴容後的數組和threshold
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
接下來對每段代碼進行分析,總共分成3個片段,分別對應上述代碼中的3中情況。
①Map不爲空,裏面存在鍵值對
代碼片段:
if (oldCap > 0) { // 此時Map不爲空,裏面存在鍵值對
if (oldCap >= MAXIMUM_CAPACITY) { // oldCap >= 0x4000 0000
threshold = Integer.MAX_VALUE; // threshold = 0x7fff ffff
return oldTab;
} // newCap = oldCap * 2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
有一個問題,如下:
爲什麼要做oldCap >= MAXIMUM_CAPACITY
判斷
首先MAXIMUM_CAPACITY
的賦值爲1 << 30
,即0x4000 0000
。然而Integer.MAX_VALUE = 0x7fff ffff
,也就是說MAXIMUM_CAPACITY << 1
會變成0x8000 0000
,也就是溢出。
爲了模擬一次這種情況,特意編寫了下面的代碼,運行下面代碼的時候,JVM默認的堆大小不夠,設了一個最大堆大小爲10GB,參數爲:-Xmx10240m
。
public static void main(String[] args) throws Exception {
disableWarning();
showHashMapInfo(0x1fffffff);
System.out.println("------------------------------------");
showHashMapInfo(0x3fffffff);
}
/**輸出結果:
initialCapacity is : 1fffffff
After hashMap created, capacity is : 20000000, threshold is : 20000000
After resize(initialization), capacity is : 20000000, threshold is : 18000000
After resize(double size), capacity is : 40000000, threshold is : 7fffffff
------------------------------------
initialCapacity is : 3fffffff
After hashMap created, capacity is : 40000000, threshold is : 40000000
After resize(initialization), capacity is : 40000000, threshold is : 7fffffff
After resize(double size), capacity is : 40000000, threshold is : 7fffffff
**/
private static void showHashMapInfo(int capacity) throws Exception {
System.out.printf("initialCapacity is : %x\n", capacity);
HashMap<String, String> hashMap = new HashMap<>(capacity);
Class<?> hashMapClass = hashMap.getClass();
// 通過反射的方式打印hashMap的屬性值
Method resizeMethod = hashMapClass.getDeclaredMethod("resize");
Method capacityMethod = hashMapClass.getDeclaredMethod("capacity");
Field thresholdField = hashMapClass.getDeclaredField("threshold");
resizeMethod.setAccessible(true);
capacityMethod.setAccessible(true);
thresholdField.setAccessible(true);
System.out.printf("After hashMap created, capacity is : %x, threshold is : %x\n", capacityMethod.invoke(hashMap), thresholdField.get(hashMap));
// 會調用resize初始化Node數組
hashMap.put("", "");
System.out.printf("After resize(initialization), capacity is : %x, threshold is : %x\n", capacityMethod.invoke(hashMap), thresholdField.get(hashMap));
// 手動再次resize
resizeMethod.invoke(hashMap);
System.out.printf("After resize(double size), capacity is : %x, threshold is : %x\n", capacityMethod.invoke(hashMap), thresholdField.get(hashMap));
}
// 去掉WARNING
private static void disableWarning() {
System.err.close();
System.setErr(System.out);
}
當在構造函數中指定了initialCapacity
後,會先將threshold
賦值成所需的capacity
。所以先看tableSizeFor()
函數,它用來將指定的initialCapacity
轉化成大於該值的2次冪。所以如果一開始capacity
的初始值是大於或等於0x40000000
的話,HashMap
指定的capacity
就是0x40000000
。源代碼如下:
// 1
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 此處並未設置capacity,只是將期望的capacity賦值給了threshold
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
// initialCapacity超過0x40000000,只取0x40000000
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
②Map爲空,但指定了初始大小
這種情況對應於上面代碼中的HashMap<String, String> hashMap = new HashMap<>(capacity);
。這種情況下會將指this.threshold = tableSizeFor(initialCapacity);
,導致int oldThr = threshold;
能夠獲取到值,所以if (oldThr > 0)
成立,將newCap = oldThr
,實現了在第一次resize()
初始化數組時,按照指定的initialCapacity
new出數組。
③Map爲空,未指定任何參數
這種情況使用默認參數:
newCap = DEFAULT_INITIAL_CAPACITY; // 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 16 * 0.75
得到newCap
和newThr
之後便開始初始化數組大小。初始化數組後,會將原數組中的Node
鏈表,按照某種規則遷移到新的數組上面去。
擴容後的調整
接下來進入數據遷移部分。這部分主要的問題是擴容後的位置怎麼算。如果oldTab == null
,那麼此次resize
屬於第一次初始化HashMap
中的數組,直接返回剛new出來的新數組即可;當原數組中的第i
爲存在Node
時,新的位置分佈存在兩種情況,即Node
有無後續Node
。這兩種情況下,在原數組中的位置的計算方式是一樣的,即:(n - 1) & hash
,其中n = tab.length
,所以位置也就是Node的hash值與上length - 1
。
// HashMap.putVal()
n = tab.length
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
無後續Node擴容後的位置
新的位置爲:e.hash & (newCap - 1)
代碼爲:newTab[e.hash & (newCap - 1)] = e;
與之前的位置相比,可能是原位,可能是oldCap + 原位
有後續Node擴容後的位置
這條鏈表上面Node的hash值的與上oldCap
結果的含義是:oldCap
的最高非0位對應在hash上的那一位到底是0還是1。因爲oldCap是0x400之類的值,只有最高位是1,其他位都是0。
如果結果是0,位置爲:原位;
如果結果是1,位置爲:原位+oldCap
只不過在新位置上之後,可能還是以一個鏈表的形式存在。