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