HashMap的容量與擴容

有幾個重要的常量:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//默認的桶數組大小
static final int MAXIMUM_CAPACITY = 1 << 30;//極限值(超過這個值就將threshold修改爲Integer.MAX_VALUE(此時桶大小已經是2的31次方了),表明不進行擴容了)
static final float DEFAULT_LOAD_FACTOR = 0.75f;//負載因子(請閱讀下面體會這個值的用處)

(一)調用HashMap()構造方法

這個構造方法長這樣:

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; //設置一個默認的負載因子,默認爲0.75,下面說它有啥用
}

這就完了,然後用於hash的桶數組爲null,當我們第一次put的時候,會到達這段代碼:

Node<K,V>[] tab; int n;
    //table就是桶數組,初始爲null
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

所以會進入resize()方法,到達這段代碼:

//取消多餘的代碼,我直接貼上進入的這個分支的代碼
else {
      newCap = DEFAULT_INITIAL_CAPACITY;
      newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}

newCap的意思是:即將用於hash的桶數組的長度,這裏的默認值是16
newThr的意思是:當桶中的鍵值對的個數超過這個值時就進行擴容,這時候上面提到的負載因子就起作用了,所以這裏的newThr爲0.75×16 = 12

所以經過這個默認構造方法,並且進行第一次put後,桶數組建立了,桶容量爲16,極限值爲12。

(二)調用HashMap(int initialCapacity)構造方法

public HashMap(int initialCapacity) {
    //這個方法實際調用另一個構造方法,所以這個構造方法就不分析了,直接看第三個的分析
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

(三)調用HashMap(int initialCapacity, float loadFactor)構造方法

public HashMap(int initialCapacity, float loadFactor) {
    //桶數組的大小小於0時拋異常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
    //如果桶數組的大小超過最大值,則簡單的將桶容量修改爲最大值2的30次方
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //如果負載因子不符合規範,那麼拋異常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
    this.loadFactor = loadFactor;
    //這個意思是根據桶數組的大小求一個“極限值threshold”,當桶中的鍵值隊個數超過這個大小就進行擴容
    //tableSizeFor方法求得的數字是剛超過initialCapacity的一個2的n次方的數,例如initialCapacity是1000,那麼得到的threshold就是1024
    this.threshold = tableSizeFor(initialCapacity);
    }

所以經過這個方法之後,桶容量確定了,極限值也確定了,但是桶數組還是null。
當第一次put時,觸發如下代碼:

//發現桶數組是null,所以需要新建一個桶數組
if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

接着看resize()中觸發了哪些代碼:

Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;//看這裏,oldCap會是0
int oldThr = threshold;//看這裏,oldThr不會是0,因爲有tableSizeFor()方法,確保oldThr至少是1
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
}
//所以肯定會進入這個分支,將桶數組的大小改爲極限值大小。然後在下面創建一個newCap大小的桶數組
else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;

然後還會進入下面這個分支:

if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }

這個分支會按負載因子設置極限值

(四)超過極限值後的擴容

在每次put之後,會有下面這個判斷:

if (++size > threshold)
    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;
    }
    //如果我們是這樣調用:HashMap(7,任意滿足條件的值),那麼經過第一次調用put()會初始化桶數組的大小和極限值一樣,這裏即爲8。所以這裏不會進入這個分支,但是newCap會變爲16
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
            oldCap >= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr << 1; // double threshold
}
//所以會進入這裏
if (newThr == 0) {
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
            (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;//計算的出這裏的threshold即極限值會由剛纔的8變爲12(和負載因子大小有關)

接上面這個例子,下一次擴容會如何?

if (oldCap > 0) {
    if (oldCap >= MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return oldTab;
    }
    //因爲桶容量變爲了16,所以這次會進入這個分支,桶容量和極限值都會加倍。
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
            oldCap >= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr << 1; // double threshold
}

直到…………
當桶容量加倍到最大值會怎麼樣?

//因爲每次都是加倍,所以最終肯定會加倍到MAXIMUM_CAPACITY,會進入這個分支。
if (oldCap >= MAXIMUM_CAPACITY) {
    threshold = Integer.MAX_VALUE;
    return oldTab;
}

桶容量變爲最大值時,緊接着的一次擴容只是簡單的將極限值修改爲Integer.MAX_VALUE。桶數組大小並不會繼續加倍

(五)如果HashMap中的鍵值對數量超過Integer.MAX_VALUE了呢?

我們仔細看一下這段代碼:

//這個size類型是int,那麼最大就是Integer.MAX_VALUE,所以不會有++size > threshold的情況,往後都不會進行擴容了。
if (++size > threshold)
    resize();
發佈了74 篇原創文章 · 獲贊 213 · 訪問量 29萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章