阿里面試:HashMap是如何實現擴容?哪三步?
1.達到閾值開始擴容
如下圖所示:
圖片
HashMap的擴容機制是當HashMap中的元素個數超過了負載因子(loadFactor)與初始容量(initialCapacity)的乘積時,就會觸發擴容機制。
默認的構造函數指定了擴容因子:0.75, 默認容量是16,如下所示:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 相當于16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
元素數量達到了閾值(即負載因子 * 桶的數量)時,會觸發擴容操作,也就是說第1次擴容的動作會在元素個數達到12(16*0.75)的時候觸發擴容。
為了盡可能地減少擴容操作的次數,通常會將負載因子設置為一個較小的值,例如0.75,以保證哈希表的容量能夠滿足存儲需求,同時又不會造成太多的空間浪費。
2.開始擴容執行
HashMap通過resize()方法進行擴容,容量規則為2的冪次。
如下所示:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//以前的容量大于0,也就是hashMap中已經有元素了,或者new對象的時候設置了初始容量
if (oldCap > 0) {
//如果以前的容量大于限制的最大容量1<<30,則設置臨界值為int的最大值2^31-1
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
/**
* 如果以前容量的2倍小于限制的最大容量,同時大于或等于默認的容量16,則設置臨界值為以前臨界值的2
* 倍,因為threshold = loadFactor*capacity,capacity擴大了2倍,loadFactor不變,
* threshold自然也擴大2倍。
*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
/**
* 在HashMap構造器Hash(int initialCapacity, float loadFactor)中有一句代碼,this.threshold
* = tableSizeFor(initialCapacity), 表示在調用構造器時,默認是將初始容量暫時賦值給了
* threshold臨界值,因此此處相當于將上一次的初始容量賦值給了新的容量。什么情況下會執行到這句?當調用
* 了HashMap(int initialCapacity)構造器,還沒有添加元素時
*/
else if (oldThr > 0)
newCap = oldThr;
/**
* 調用了默認構造器,初始容量沒有設置,因此使用默認容量DEFAULT_INITIAL_CAPACITY(16),臨界值
* 就是16*0.75
*/
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//對臨界值做判斷,確保其不為0,因為在上面第二種情況(oldThr > 0),并沒有計算newThr
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
/**構造新表,初始化表中數據*/
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//將剛創建的新表賦值給table
table = newTab;
if (oldTab != null) {
//遍歷將原來table中的數據放到擴容后的新表中來
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//沒有鏈表Node節點,直接放到新的table中下標為【e.hash & (newCap - 1)】位置即可
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果是treeNode節點,則樹上的節點放到newTab中
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果e后面還有鏈表節點,則遍歷e所在的鏈表,
else { // 保證順序
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
//記錄下一個節點
next = e.next;
/**
* newTab的容量是以前舊表容量的兩倍,因為數組table下標并不是根據循環逐步遞增
* 的,而是通過(table.length-1)& hash計算得到,因此擴容后,存放的位置就
* 可能發生變化,那么到底發生怎樣的變化呢,就是由下面的算法得到.
*
* 通過e.hash & oldCap來判斷節點位置通過再次hash算法后,是否會發生改變,如
* 果為0表示不會發生改變,如果為1表示會發生改變。到底怎么理解呢,舉個例子:
* e.hash = 13 二進制:0000 1101
* oldCap = 32 二進制:0001 0000
* &運算:0 二進制:0000 0000
* 結論:元素位置在擴容后不會發生改變
*/
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
/**
* e.hash = 18 二進制:0001 0010
* oldCap = 32 二進制:0001 0000
* &運算:32 二進制:0001 0000
* 結論:元素位置在擴容后會發生改變,那么如何改變呢?
* newCap = 64 二進制:0010 0000
* 通過(newCap-1)&hash
* 即0001 1111 & 0001 0010 得0001 0010,32+2 = 34
*/
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
/**
* 若(e.hash & oldCap) == 0,下標不變,將原表某個下標的元素放到擴容表同樣
* 下標的位置上
*/
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
/**
* 若(e.hash & oldCap) != 0,將原表某個下標的元素放到擴容表中
* [下標+增加的擴容量]的位置上
*/
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
擴容會將HashMap的容量(即桶的數量)翻倍,擴容的大小是原來的2倍,并重新計算每個元素在新桶中的位置。
3.新建哈希表存儲擴容
擴容操作需要創建一個新的哈希表,并將舊哈希表中的元素重新分配到新哈希表的桶中。
重新分配元素時,HashMap會對每個元素的哈希值取模得到一個新的桶位置,并將元素插入到新的桶中。
擴容操作完成后,HashMap將使用新的哈希表來存儲元素,并釋放舊哈希表的內存空間。