轉自:http://www.cnblogs.com/yanzige/p/8392142.html
(一) Java 7 中Hashmap擴容機制
一、什麼時候擴容:
網上總結的會有很多,但大多都總結的不夠完整或者不夠準確。大多數可能值說了滿足我下麪條件一的情況。
擴容必須滿足兩個條件:
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個了,則會將該樹轉爲鏈表。