深入理解HashMap的擴容機制
Java 7 中Hashmap擴容機制
原文連結:https://www.cnblogs.com/yanzige/p/8392142.html
一、什麼時候擴容:
網上總結的會有很多,但大多都總結的不夠完整或者不夠準確。大多數可能值說了滿足我下面條件一的情況。
擴容必須滿足兩個條件:
1、 存放新值的時候當前已有元素的個數必須大於等於閾值
2、 存放新值的時候當前存放資料發生hash碰撞(當前key計算的hash值換算出來的陣列下標位置已經存在值)
二、下面我們看原始碼,如下:
首先是put()方法
public V put(K key, V value) { //判斷當前Hashmap(底層是Entry陣列)是否存值(是否為空陣列)
if (table == EMPTY_TABLE) {
inflateTable(threshold); //如果為空,則初始化
}
//判斷key是否為空
if (key == null )
return putForNullKey(value); //hashmap允許key為空
//計算當前key的雜湊值
int hash = hash(key);
//通過雜湊值和當前資料長度,算出當前key值對應在陣列中的存放位置
int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null ; e = e.next) {
Object k;
//如果計算的雜湊位置有值(及hash衝突),且key值一樣,則覆蓋原值value,並返回原值value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess( this );
return oldValue;
}
}
modCount++;
//存放值的具體方法
addEntry(hash, key, value, i);
return null ;
}
|
在put()方法中有呼叫addEntry()方法,這個方法裡面是具體的存值,在存值之前還要判斷是否需要擴容
void addEntry( int hash, K key, V value, int bucketIndex) {
//1、判斷當前個數是否大於等於閾值
//2、當前存放是否發生雜湊碰撞
//如果上面兩個條件否發生,那麼就擴容
if ((size >= threshold) && ( null != table[bucketIndex])) {
//擴容,並且把原來陣列中的元素重新放到新陣列中
resize( 2 * table.length);
hash = ( null != key) ? hash(key) : 0 ;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
|
如果需要擴容,呼叫擴容的方法resize()
void resize( int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//判斷是否有超出擴容的最大值,如果達到最大值則不進行擴容操作
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return ;
}
Entry[] newTable = new Entry[newCapacity];
// transfer()方法把原陣列中的值放到新陣列中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//設定hashmap擴容後為新的陣列引用
table = newTable;
//設定hashmap擴容新的閾值
threshold = ( int )Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1 );
}
|
transfer()在實際擴容時候把原來陣列中的元素放入新的陣列中
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while ( null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//通過key值的hash值和新陣列的大小算出在當前陣列中的存放位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
|
三、總結:
Hashmap的擴容需要滿足兩個條件:當前資料儲存的數量(即size())大小必須大於等於閾值;當前加入的資料是否發生了hash衝突。
因為上面這兩個條件,所以存在下面這些情況
(1)、就是hashmap在存值的時候(預設大小為16,負載因子0.75,閾值12),可能達到最後存滿16個值的時候,再存入第17個值才會發生擴容現象,因為前16個值,每個值在底層陣列中分別佔據一個位置,並沒有發生hash碰撞。
(2)、當然也有可能儲存更多值(超多16個值,最多可以存26個值)都還沒有擴容。原理:前11個值全部hash碰撞,存到陣列的同一個位置(雖然hash衝突,但是這時元素個數小於閾值12,並沒有同時滿足擴容的兩個條件。所以不會擴容),後面所有存入的15個值全部分散到陣列剩下的15個位置(這時元素個數大於等於閾值,但是每次存入的元素並沒有發生hash碰撞,也沒有同時滿足擴容的兩個條件,所以葉不會擴容),前面11+15=26,所以在存入第27個值的時候才同時滿足上面兩個條件,這時候才會發生擴容現象。
Java 8中Hashmap擴容機制
一、Java8的擴容機制:
Java8不再像Java7中那樣需要滿足兩個條件,Java8中擴容只需要滿足一個條件:當前存放新值(注意不是替換已有元素位置時)的時候已有元素的個數大於等於閾值(已有元素等於閾值,下一個存放後必然觸發擴容機制)
注:
(1)擴容一定是放入新值的時候,該新值不是替換以前位置的情況下(說明:put(“name”,"zhangsan"),而map裡面原有資料<"name","lisi">,則該存放過程就是替換一個原有值,而不是新增值,則不會擴容)
(2)擴容發生在存放後,即是資料存放後(先存放後擴容),判斷當前存入物件的個數,如果大於閾值則進行擴容。
二、背靜知識:
Java7中Hashmap底層採用的是Entry對陣列,而每一個Entry對又向下延伸是一個連結串列,在連結串列上的每一個Entry對不僅儲存著自己的key/value值,還存了前一個和後一個Entry對的地址。
Java8中的Hashmap底層結構有一定的變化,還是使用的陣列,但是陣列的物件以前是Entry對,現在換成了Node物件(可以理解是Entry對,結構一樣,儲存時也會存key/value鍵值對、前一個和後一個Node的地址),以前所有的Entry向下延伸都是連結串列,Java8變成連結串列和紅黑樹的組合,資料少量存入的時候優先還是連結串列,當連結串列長度大於8,且總資料量大於64的時候,連結串列就會轉化成紅黑樹,所以你會看到Java8的Hashmap的資料儲存是連結串列+紅黑樹的組合,如果資料量小於64則只有連結串列,如果資料量大於64,且某一個數組下標資料量大於8,那麼該處即為紅黑樹。
三、原始碼:
在jdk7中,當new Hashmap()的時候會對物件進行初始化,而jdk8中new Hashmap()並沒有對物件進行初始化,而是在put()方法中通過判斷物件是否為空,如果為空通過呼叫resize()來初始化物件。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
/** * Implements Map.put and related methods * * @param hash key值計算傳來的下標 * @param key * @param value * @param onlyIfAbsent true只是在值為空的時候儲存資料,false都儲存資料 * @param evict * @return 返回被覆蓋的值,如果沒有覆蓋則返回null */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // 申明entry陣列物件tab[]:當前Entry[]物件 Node<K,V>[] tab; // 申明entry物件p:這裡表示存放的單個節點 Node<K,V> p; // n:為當前Entry物件長度
// i:為當前存放物件節點的位置下標 int n, i; /** * 流程判斷 * 1、如果當前Node陣列(tab)為空,則直接建立(通過resize()建立),並將當前建立後的長度設定給n * 2、如果要存放物件所在位置的Node節點為空,則直接將物件存放位置建立新Node,並將值直接存入 * 3、存放的Node陣列不為空,且存放的下標節點Node不為空(該Node節點為連結串列的首節點) * 1)比較連結串列的首節點存放的物件和當前存放物件是否為同一個物件,如果是則直接覆蓋並將原來的值返回 * 2)如果不是分兩種情況 * (1)儲存處節點為紅黑樹node結構,呼叫方法putTreeVal()直接將資料插入 * (2)不是紅黑樹,則表示為連結串列,則進行遍歷 * A.如果存入的連結串列下一個位置為空,則先將值直接存入,存入後檢查當前存入位置是否已經大於連結串列的第8個位置 * a.如果大於,呼叫treeifyBin方法判斷是擴容 還是 需要將該連結串列轉紅黑樹(大於8且總資料量大於64則轉紅黑色,否則對陣列進行擴容) * b.當前存入位置連結串列長度沒有大於8,則存入成功,終端迴圈操作。 * B.如果存入連結串列的下一個位置有值,且該值和存入物件“一樣”,則直接覆蓋,並將原來的值返回 * 上面AB兩種情況執行完成後,判斷返回的原物件是否為空,如果不為空,則將原物件的原始value返回 * 上面123三種情況下,如果沒有覆蓋原值,則表示新增存入資料,儲存資料完成後,size+1,然後判斷當前資料量是否大於閾值, * 如果大於閾值,則進行擴容。 */ if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) // 按照紅黑樹直接將資料存入 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash);//該方法判斷是擴容還是需要將該連結串列轉紅黑樹 break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 如果不是替換資料存入,而是新增位置存入後,則將map的size進行加1,然後判斷容量是否超過閾值,超過則擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
treeifyBin()方法判斷是擴容還是將當前連結串列轉紅黑樹
/** * Replaces all linked nodes in bin at index for given hash unless * table is too small, in which case resizes instead. * 從指定hash位置處的連結串列nodes頭部開始,全部替換成紅黑樹結構。 * 除非整個陣列物件(Map集合)資料量很小(小於64),該情況下則通過resize()對這個Map進行擴容,而代替將連結串列轉紅黑樹的操作。 */ final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) { int n, index; HashMap.Node<K,V> e; // 如果Map為空或者當前存入資料n(可以理解為map的size())的數量小於64便進行擴容 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); // 如果size()大於64則將正在存入的該值所在連結串列轉化成紅黑樹 else if ((e = tab[index = (n - 1) & hash]) != null) { HashMap.TreeNode<K,V> hd = null, tl = null; do { HashMap.TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
四、總結:
(1)Java 8 在新增資料存入成功後進行擴容
(2)擴容會發生在兩種情況下(滿足任意一種條件即發生擴容):
a 當前存入資料大於閾值即發生擴容
b存入資料到某一條連結串列上,此時資料大於8,且總數量小於64即發生擴容
(3)此外需要注意一點java7是在存入資料前進行判斷是否擴容,而java8是在存入資料庫在進行擴容的判斷。
ConcurrentHashMap知識參考:https://www.cnblogs.com/zerotomax/p/8687425.html
Java8 HashMap擴容可參考:https://blog.csdn.net/goosson/article/details/81029729(注:該文章中關於Java8底層資料結構描述不準確,只有當資料量大於64才會有紅黑樹+連結串列)
這裡補充一下jdk8關於紅黑樹和連結串列的知識:
第一次新增元素的時候,預設初期長度為16,當往map中繼續新增元素的時候,通過hash值跟陣列長度取“與”來決定放在陣列的哪個位置,如果出現放在同一個位置的時候,優先以連結串列的形式存放,在同一個位置的個數又達到了8個(程式碼是>=7,從0開始,及第8個開始判斷是否轉化成紅黑樹),如果陣列的長度還小於64的時候,則會擴容陣列。如果陣列的長度大於等於64的話,才會將該節點的連結串列轉換成樹。在擴容完成之後,如果某個節點的是樹,同時現在該節點的個數又小於等於6個了,則會將該樹轉為連結串列。