HashMap源代碼分析-II-擴容策略詳細分析

距離寫第一篇關於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()初始化數組時,按照指定的initialCapacitynew出數組。

③Map爲空,未指定任何參數

這種情況使用默認參數:

newCap = DEFAULT_INITIAL_CAPACITY; // 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 16 * 0.75

得到newCapnewThr之後便開始初始化數組大小。初始化數組後,會將原數組中的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

只不過在新位置上之後,可能還是以一個鏈表的形式存在。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章