1. 程式人生 > >HashMap的容量與擴容

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;//負載因子(請閱讀下面體會這個值的用處)
  • 1
  • 2
  • 3

(一)呼叫HashMap()構造方法

這個構造方法長這樣:

public HashMap
() { this.loadFactor = DEFAULT_LOAD_FACTOR; //設定一個預設的負載因子,預設為0.75,下面說它有啥用 }
  • 1
  • 2
  • 3

這就完了,然後用於hash的桶陣列為null,當我們第一次put的時候,會到達這段程式碼:

Node<K,V>[] tab; int n;
    //table就是桶陣列,初始為null
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
  • 1
  • 2
  • 3
  • 4

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

//取消多餘的程式碼,我直接貼上進入的這個分支的程式碼
else {
      newCap = DEFAULT_INITIAL_CAPACITY;
      newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
  • 1
  • 2
  • 3
  • 4
  • 5

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

所以經過這個預設構造方法,並且進行第一次put後,桶陣列建立了,桶容量為16,極限值為12。

(二)呼叫HashMap(int initialCapacity)構造方法

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

(三)呼叫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);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

//發現桶陣列是null,所以需要新建一個桶陣列
if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
  • 1
  • 2
  • 3

接著看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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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

if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
  • 1
  • 2
  • 3
  • 4
  • 5

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

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

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

if (++size > threshold)
    resize();
  • 1
  • 2

就是說超過極限值時會進行擴容,擴容方式如下:

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(和負載因子大小有關)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

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

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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

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

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

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

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

我們仔細看一下這段程式碼:

//這個size型別是int,那麼最大就是Integer.MAX_VALUE,所以不會有++size > threshold的情況,往後都不會進行擴容了。
if (++size > threshold)
    resize();
  • 1
  • 2
  • 3