[HashMap原始碼學習之路]---陣列擴容後元素的前後變化
HashMap陣列擴容後元素的前後變化
前一段時間看了HashMap
的擴容方法,覺得寫的太好了,對我很有幫助,現以我理解的來寫一下。主要說兩方面:
- 擴容後元素的位置
- 擴容後元素如何分佈的
1、resize方法的原始碼
HashMap
中擴容方法為resize()
。程式碼如下:
final Node<K,V>[] 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;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1 ; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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 = newTab;
//說明①
//如果原陣列有資料,說明不是首次初始化陣列,則會造成擴容,
//元素重新分佈的問題,消耗效能。
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
//說明②
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//不講紅黑樹的部分了
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//說明③
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//說明④
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
上邊是整個resize()
方法的原始碼,今天說的是其中的這部分,即從上邊帶有註釋的說明①
位置下邊的if (oldTab != null) {
這一行開始。
從這一行的判斷裡看到,意思是原有table
裡如果沒有元素的話,會走的邏輯,其實就是hashMap
針對於陣列長度達到負載因子*陣列長度
的時候,會進行資料的重新分佈,這一步上也是效能消耗的地方。
2、擴容後陣列元素的位置(無鏈)
先來看上邊原始碼的說明②
位置,即下邊這行:
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
這行的意思是,如果當前陣列下標上有元素,但是沒有子元素,也就是沒有形成鏈,就只有一個元素
,那麼該元素放置的位置按照newTab[e.hash & (newCap - 1)]
來放置。
首先來看一張圖,hashMap
中,確定元素在陣列哪個位置,是通過hash
值(通過一個hash方法進一步運算過的)與陣列長度減一進行與運算
得到的,如下圖所示:
做法特別巧妙,那如果擴容後,比如對上邊的陣列長度16
,現在變成了長度為32
,也就是上邊提到的這個newTab[e.hash & (newCap - 1)]
,相同的三個數,在擴容後的放置位置是什麼呢?看下圖:
為什麼會出現不同的位置呢,請仔細看那兩個hash
數,對應的100000
中的1
上的數,有的有1
,有的有0
,因為這是個隨機數,並不知道到底是幾,反正不是0
,就是1
了,這樣就會使算出來的下標,分到兩個位置上,使擴容後的陣列,對元素又均勻的分攤一下。
總結:HashMap
擴容後,原來的元素,要麼在原位置,要麼在原位置+原陣列長度
那個位置上。
2、擴容後元素的位置(有鏈)
借用別人的圖,如下:
通過上圖來看,左邊是擴容前
,右邊是擴容後
,對於下標為15
上整條鏈上的元素,擴容一倍後,元素要麼在原位置15
上,要麼在原位置加原陣列長度,即15+16
那個位置上。並且整條鏈上的元素,不管在原位置15
,還是在31
上,它們的排列順序沒有變化。(據說這與1.7版本不同,我沒看過1.7版本)
那是如何做到的呢,再繼續看上邊的原始碼,位置在上邊原始碼的說明③
處,即下邊這個位置:
do {
next = e.next;
//說明③
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//說明④
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
上邊的程式碼中,根據說明③
處的(e.hash & oldCap) == 0
來將鏈上的元素分成兩份,然後又在說明④
處,對元素分別放到了原位置
和原位置+原陣列長度
上。
用幾個資料來舉個例子,就會如下圖所示:
比如一個鏈上的資料如下圖,三種顏色,形成了鏈。
如果對於它們三個,擴容一倍後,就會變成下邊這樣:
理解的不對的話,請大神及時指正。