雖然在hashmap的原理里面有這段纫雁,但是這個單獨拿出來講rehash或者resize()也是極好的澳盐。
什么時候擴容:當(dāng)向容器添加元素的時候锅论,會判斷當(dāng)前容器的元素個數(shù)讼溺,如果大于等于閾值(知道這個閾字怎么念嗎?不念fa值最易,念yu值四聲)---即當(dāng)前數(shù)組的長度乘以加載因子的值的時候怒坯,就要自動擴容啦。
擴容(resize)就是重新計算容量藻懒,向HashMap對象里不停的添加元素剔猿,而HashMap對象內(nèi)部的數(shù)組無法裝載更多的元素時,對象就需要擴大數(shù)組的長度嬉荆,以便能裝入更多的元素归敬。當(dāng)然Java里的數(shù)組是無法自動擴容的,方法是使用一個新的數(shù)組代替已有的容量小的數(shù)組鄙早,就像我們用一個小桶裝水汪茧,如果想裝更多的水,就得換大水桶限番。
先看一下什么時候舱污,resize();
我們分析下resize的源碼,鑒于JDK1.8融入了紅黑樹弥虐,較復(fù)雜扩灯,為了便于理解我們?nèi)匀皇褂肑DK1.7的代碼媚赖,好理解一些,本質(zhì)上區(qū)別不大驴剔,具體區(qū)別后文再說省古。
這里就是使用一個容量更大的數(shù)組來代替已有的容量小的數(shù)組,transfer()方法將原有Entry數(shù)組的元素拷貝到新的Entry數(shù)組里丧失。
文章中間部分:四豺妓、存儲實現(xiàn);詳細(xì)解釋了為什么indexFor方法中要h & (length-1)
newTable[i]的引用賦給了e.next布讹,也就是使用了單鏈表的頭插入方式琳拭,同一位置上新元素總會被放在鏈表的頭部位置;這樣先放在一個索引上的元素終會被放到Entry鏈的尾部(如果發(fā)生了hash沖突的話)描验,這一點和Jdk1.8有區(qū)別白嘁,下文詳解。在舊數(shù)組中同一條Entry鏈上的元素膘流,通過重新計算索引位置后絮缅,有可能被放到了新數(shù)組的不同位置上。
從上面的for循環(huán)內(nèi)部開始說起吧:詳細(xì)解釋下呼股,這個轉(zhuǎn)存的過程耕魄。和怎么個頭插入法.
Entry e = src[j];
這句話,就把原來數(shù)組上的那個鏈表的引用就給接手了彭谁,所以下面src[j] = null;可以放心大膽的置空吸奴,釋放空間。告訴gc這個地方可以回收啦缠局。
繼續(xù)到do while 循環(huán)里面则奥,
Entry next = e.next;
int i = indexFor(e.hash, newCapacity);計算出元素在新數(shù)組中的位置
下面就是單鏈表的頭插入方式轉(zhuǎn)存元素啦
關(guān)于這個 單鏈表的頭插入方式 的理解,我多說兩句狭园。
這地方我再看的時候读处,就有點蒙了,他到底怎么在插到新的數(shù)組里面的妙啃?
要是在插入新數(shù)組的時候档泽,也出現(xiàn)了一個數(shù)組下標(biāo)的位置處,出現(xiàn)了多個節(jié)點的話揖赴,那又是怎么插入的呢馆匿?
1,假設(shè)現(xiàn)在剛剛插入到新數(shù)組上燥滑,因為是對象數(shù)組渐北,數(shù)組都是要默認(rèn)有初始值的,那么這個數(shù)組的初始值都是null铭拧。不信的可以新建個Javabean數(shù)組測試下赃蛛。
那么e.next = newTable[i],也就是e.next = null啦恃锉。然后再newTable[i] = e;也就是 說這個時候,這個數(shù)組的這個下標(biāo)位置的值設(shè)置成這個e啦呕臂。
2破托,假設(shè)這個時候,繼續(xù)上面的循環(huán)歧蒋,又取第二個數(shù)據(jù)e2的時候土砂,恰好他的下標(biāo)和剛剛上面的那個下標(biāo)相同啦,那么這個時候谜洽,是又要有鏈表產(chǎn)生啦萝映、
e.next = newTable[i];,假設(shè)上面第一次存的叫e1吧阐虚,那么現(xiàn)在e.next = newTable[i];也就是e.next = e1序臂;
然后再,newTable[i] = e;实束,把這個后來的賦值在數(shù)組下標(biāo)為i的位置奥秆,當(dāng)然他們兩個的位置是相同的啦。然后注意現(xiàn)在的e咸灿,我們叫e2吧吭练。e2.next指向的是剛剛的e1,e1的next是null析显。
這就解釋啦:先放在一個索引上的元素終會被放到Entry鏈的尾部。這句話签赃。
關(guān)于什么時候resize()的說明:
看1.7的源碼上說的條件是:
if ((size >= threshold) && (null != table[bucketIndex])) {谷异。。锦聊。}
其中
size表示當(dāng)前hashmap里面已經(jīng)包含的元素的個數(shù)歹嘹。
threshold:threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
一般是容量值X加載因子。
而1.8的是:
if (++size > threshold){}
其中
size:The number of key-value mappings contained in this map.和上面的是一樣的
threshold:newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
也是一樣的孔庭,
總結(jié)一下:就是這個map里面包含的元素尺上,也就是size的值,大于等于這個閾值的時候圆到,才會resize()怎抛;
具體到實際情況就是:假設(shè)現(xiàn)在閾值是4;在添加下一個假設(shè)是第5個元素的時候芽淡,這個時候的size還是原來的马绝,還沒加1,size=4挣菲,那么閾值也是4的時候富稻,
當(dāng)執(zhí)行put方法掷邦,添加第5個的時候,這個時候椭赋,4 >= 4抚岗。元素個數(shù)等于閾值。就要resize()啦哪怔。添加第4的時候宣蔚,還是3 >= 4不成立,不需要resize()蔓涧。
經(jīng)過這番解釋件已,可以發(fā)現(xiàn)下面的這個例子,不應(yīng)該是在添加第二個的時候resize()元暴,而是在添加第三個的時候篷扩,才resize()的。
這個也是我后來再細(xì)看的時候茉盏,發(fā)現(xiàn)的鉴未。當(dāng)然,這個咱可以先忽略鸠姨,重點看如何resize()铜秆,以及如何將舊數(shù)據(jù)移動到新數(shù)組的
下面舉個例子說明下擴容過程。
這句話是重點----hash(){return key % table.length;}方法,就是翻譯下面的一行解釋:
假設(shè)了我們的hash算法就是簡單的用key mod 一下表的大醒惹ā(也就是數(shù)組的長度)连茧。
其中的?哈希桶數(shù)組table的size=2, 所以key = 3巍糯、7荣德、5触机,put順序依次為 5、7、3窒升。在mod 2以后都沖突在table[1]這里了侦香。這里假設(shè)負(fù)載因子 loadFactor=1脖卖,即當(dāng)鍵值對的實際大小size 大于 table的實際大小時進行擴容窗声。接下來的三個步驟是哈希桶數(shù)組 resize成4,然后所有的Node重新rehash的過程厌衙。
下面我們講解下JDK1.8做了哪些優(yōu)化距淫。經(jīng)過觀測可以發(fā)現(xiàn),我們使用的是2次冪的擴展(指長度擴為原來2倍)婶希,所以溉愁,
經(jīng)過rehash之后,元素的位置要么是在原位置,要么是在原位置再移動2次冪的位置拐揭。對應(yīng)的就是下方的resize的注釋撤蟆。
看下圖可以明白這句話的意思,n為table的長度堂污,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例家肯,圖(b)表示擴容后key1和key2兩種key確定索引位置的示例,其中hash1是key1對應(yīng)的哈希值(也就是根據(jù)key1算出來的hashcode值)與高位與運算的結(jié)果盟猖。
元素在重新計算hash之后讨衣,因為n變?yōu)?倍,那么n-1的mask范圍在高位多1bit(紅色)式镐,因此新的index就會發(fā)生這樣的變化:
因此反镇,我們在擴充HashMap的時候,不需要像JDK1.7的實現(xiàn)那樣重新計算hash娘汞,只需要看看原來的hash值新增的那個bit是1還是0就好了歹茶,是0的話索引沒變,是1的話索引變成“原索引+oldCap”你弦,可以看看下圖為16擴充為32的resize示意圖:
我是 沒看懂他連個圖是怎么前后對應(yīng)的惊豺,誰看懂了,交流哈賽禽作。
當(dāng)時上面這個圖尸昧,沒看懂,是因為旷偿,他就沒說每個節(jié)點的hashcode是啥烹俗,他怎么確定是保留在原來的位置,還是說在原來位置的基礎(chǔ)上再加個原來數(shù)組的長度呢萍程。所以衷蜓,上面那個圖僅僅具有丁點兒參考價值。
這個設(shè)計確實非常的巧妙尘喝,既省去了重新計算hash值的時間,而且同時斋陪,由于新增的1bit是0還是1可以認(rèn)為是隨機的朽褪,因此resize的過程,均勻的把之前的沖突的節(jié)點分散到新的bucket了无虚。這一塊就是JDK1.8新增的優(yōu)化點缔赠。有一點注意區(qū)別,JDK1.7中rehash的時候友题,舊鏈表遷移新鏈表的時候嗤堰,如果在新表的數(shù)組索引位置相同,則鏈表元素會倒置度宦,但是從上圖可以看出踢匣,JDK1.8不會倒置告匠。有興趣的同學(xué)可以研究下JDK1.8的resize源碼,寫的很贊离唬。