點選關註公眾號,Java幹貨 及時送達 👇
引言
最近在翻看【阿裏巴巴開發手冊-嵩山版】時,發現其修正了關於HashMap關於1024個元素擴容的次數 在先前的版本泰山版我們可以看到以下描述:
而嵩山版則可以看到:
同時我們也可在嵩山版的版本歷史中看到對以上變化的描述:
並且我在官網文件中也同樣發現了采訪視訊對這一變化,主持人對孤盡老師提出了以下疑問:
主持人:
有同學問,嵩山版修正了HashMap關於1024個元素擴容的次數?那麽這個泰山版中是七次擴容,我感覺是正確的,為什麽現在進行了修改?
孤盡老師:
「
大家可以看到嵩山版描述都改了,就沒有講「擴容」,講叫「resize」的次數。因為resize的這個次數的話,我們在調resize的時候,就put,如果你new了一個HashMap它並不會給你分配空間,第一次put的時候,它才會給你分配空間。所以就是說我們在第一次put的時候,也會調resize。但是很多同學就會爭議,說這個算不算擴容?那其實就是說我們在這個點上,其實為了,因為電腦我們大家都是搞電腦的,都希望用沒有「二義性」的語言來表示。所以在嵩山版本裏面進行了修改,然後我們改成了resize的方式來說明這到底是它的容量是如何變化的。因為擴容這個詞的話,大家的理解是不一樣的。這也是我們有一次大概在業界我們有一個打卡就是一個打卡測試題,這個題裏面我們有一個選項。當時選的同學是兩種答案,但是這兩種答案都是有自己的道理。所以我們也是借鑒了這個看法,然後在這個嵩山版裏面也寫的更加明確了。就是叫resize的次數,不叫擴容的次數。
以下是文件下載連結和采訪視訊地址。
https://developer.aliyun.com/topic/java20
我們可以從孤盡老師的回答中提煉出主要發生的變化是:
擴容次數修改為resize次數。主要的問題是每個人對「擴容」這個定義存在分歧。
擴容的主要分歧是在:沒有給HashMap進行初始化的時候,第一次put的時候才會分配空間,也會呼叫resize。那這操作個算不算擴容?即初始化容量這個操作算不算擴容?
所以擴容次數修改為resize次數。
那麽我們先來看一下第一次put的時候呼叫resize這個操作是什麽?
第一次put呼叫resize()
前面孤盡老師的回答中也強調了這個put()方法呼叫resize()方法是存在一個條件的:
「
如果你new了一個HashMap它並不會給你分配空間。同時文件中也寫了由於沒有設定容量初始大小。
「
除了這個條件其實還有一個額外的條件,就是這是在JDK1.8之後HashMap才會有的操作。
要理解這句話我們首先要看下HashMap的源碼,看下它的構造器方法。
存在四個構造方法
//參數:初始容量,負載因子
publicHashMap(int initialCapacity, float loadFactor){ }
//參數:初始容量。呼叫上一個建構函式,預設的負載因子(0.75)
publicHashMap(int initialCapacity){ this(initialCapacity, DEFAULT_LOAD_FACTOR); }
//參數:無參構造器。建立的HashMap具有預設的負載因子(0.75)
publicHashMap(){ this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }
//參數:使用與指定Map相同的對映構造一個新的HashMap。建立的HashMap具有預設的負載因子(0.75)和足夠在指定Map中保存對映的初始容量。
publicHashMap(Map<? extends K, ? extends V> m){
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
也就是說當我們使用一個無參構造器建立HashMap的時候
Map<String,String> map = new HashMap<>();
呼叫的就是上述構造方法的第三個方法,只會設定預設的負載因子為0.75。就沒有其他操作了。。
而當我們對這個HashMap進行put()操作的時候
Map<String,String> map = new HashMap<>();
map.put("first","firstValue");
我們看一下源碼,進行了什麽操作?
public V put(K key, V value){
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict){
Node<K,V>[] tab; Node<K,V> p; int n, i;
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;
elseif (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;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
returnnull;
}
其實呼叫的是內部的putVal()方法。同時我們也可看見我們前面的構造器方法中除了設定了負載因子,就沒有其他操作了,所以是符合
if ((tab = table) == null || (n = tab.length) == 0)
,大家也可debug斷點打在這進行驗證。所以就會執行下面的程式碼
n = (tab = resize()).length;
呼叫了一次resize(),同時對n進行了賦值操作。
那麽我們透過源碼的確發現了,在JDK1.8中由於沒有設定HashMap容量初始大小,在第一次進行put()操作的時候,HashMap會呼叫一次resize()方法初始化容量為16。(resize的源碼也不是本篇文章的重點,所以我這邊會直接提供結論,並且再次強調一下JDK1.7是會初始化容量為16的,而不是在是第一次進行put()操作時初始化容量。
呼叫resize()的次數
在嵩山版中修訂了HashMap關於1024個元素擴容的次數修改為:resize()方法總共會呼叫 8 次。
那麽我們來看一下到底是哪8次?
再次重申一下本篇文章主要是講嵩山版對這個擴容次數的修改,涉及相關的源碼已在Java不得不知道的八股文之哈希表 中寫過,為了避免重復啰嗦,有水文之嫌,所以本文不會再次分析會直接給出對應的結論為已知條件進行分析。如想知道更多細節和版本差異,請移步之前的文章:
「
https://juejin.cn/post/7168631540518223903
以下是我們可以從源碼中得出的結論:
HashMap沒有初始化容量時預設負載因子為0.75,在第一次put()時會呼叫resize()進行初始化,容量為16。(JDK1.8)
HashMap中的閾值threshold為
capacity * load factor
就
容量*負載因子
。例如容量為16,那麽閾值threshold就為
16*0.75=12
。
當HashMap進行put()的時候,元素個數大於等於當前閾值的時候
size>=threshold
,會進行resize()操作。容量和閾值都會乘以2。例如初始容量為16,那麽當我們put第12個元素的時候,就會進行resize()操作。
基於以上已知的結論,那麽我們就能很容易的知道,在JDK1.8中沒有給HashMap初始化容量時,儲存1024個元素呼叫resize()的次數和時機了。(預設閾值為0.75)
所以在第7次進行resize()後,HashMap的capacity為1024了,但是當我們存放第768個元素時,
size>=threshold
就會進行第8次擴容,此時才能真正的存放下1024個元素。
所以在JDK1.8中沒有給HashMap初始化容量時,儲存1024個元素,會呼叫resize()8次。
「
其實resize()操作也是十分消耗效能的,我們儲存1024個元素的時候沒有設定初始值就會呼叫8次。如果我們給其設定了2048容量,那麽我們可以避免了。所以阿裏巴巴同時建議我們給HashMap初始化時給定容量。
總結
此番修正主要是每個人對「擴容」定義存在了分歧,在JDK1.8中如果沒有給HashMap設定初始容量,那麽在第一次put()操作的時候會進行resize()。而有的人認為這算一次擴容,有的人認為這不是一次擴容,這只是HashMap容量的初始化。
所以儲存1024的元素時:
前者的人認為擴容次數為8次。
後者的人認為擴容次數為7次。
孤盡老師說對此分歧,希望用沒有「二義性」的語言來表示,所以「擴容次數」修正為「resize次數」。
來源|juejin.cn/post/7302724955699789863
END
看完本文有收獲?請轉發分享給更多人
關註「Java編程鴨」,提升Java技能
關註Java編程鴨微信公眾號,後台回復:碼農大禮包可以獲取最新整理的技術資料一份。涵蓋Java 框架學習、架構師學習等!
文章有幫助的話,在看,轉發吧。
謝謝支持喲 (*^__^*)