本來想寫一篇關於HashMap完整的源碼分析的,結果我發現整理了一下東西是真的多,而且也怕誤人子弟,那就分析一下爲什麼阿里Java開發手冊裏爲要指定HashMap的容量吧。
讓我們帶着問題進入:
爲什麼要使用構造函數指定HashMap的容量
如果不指定會對效率造成多大的影響
其他的關於HashMap可以說的東西太多了,今天就根據阿里開發手冊做一個探討。
首先貼出阿里開發手冊1.4關於HashMap的部分:
【推薦】集合初始化時,指定集合初始值大小。 說明:HashMap 使用 HashMap(int initialCapacity) 初始化。 正例:initialCapacity = (需要存儲的元素個數 / 負載因子) + 1。注意負載因子(即loader factor)默認爲 0.75,如果暫時無法確定初始值大小,請設置爲 16(即默認值)。 反例:HashMap 需要放置 1024 個元素,由於沒有設置容量初始大小,隨着元素不斷增加,容 量 7 次被迫擴大,resize 需要重建 hash 表,嚴重影響性能。
注:要想更快的理解如下代碼最好新複習一下Key的HashCode生成規則,以及put時候如何將Key轉換成HashMap後存入transient Node<K,V>[] table; 中,在看一下如何從其中查找對應Key,你就會發現爲什麼HashMap如此之快了。
首先貼出需要了解的源碼:
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 如果舊數組容量大於0
if (oldCap > 0) {
// 如果容量大於容器最大值
if (oldCap >= MAXIMUM_CAPACITY) {
// 閥值設爲int最大值
threshold = Integer.MAX_VALUE;
// 返回舊數組,不再擴充
return oldTab;
}// 如果舊的容量*2 小於最大容量並且舊的容量大於等於默認容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 新的閥值也再舊的閥值基礎上*2
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
// 新容量等於舊閥值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 如果容量是0,閥值也是0,認爲這是一個新的數組,使用默認的容量16和默認的閥值12
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新的閥值是0,重新計算閥值
if (newThr == 0) {
// 使用新的容量 * 負載因子(0.75)
float ft = (float)newCap * loadFactor;
// 如果新的容量小於最大容量 且 閥值小於最大 則新閥值等於剛剛計算的閥值,否則新閥值爲 int 最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 將新閥值賦值給當前對象的閥值。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//以上源碼是爲了確定HashMap的大小以及可存儲多少元素
//==============================================================================
//以下代碼是將舊數據放入到擴容的新數組中
// 創建一個Node 數組,容量是新數組的容量(新容量要麼是舊的容量,要麼是舊容量*2,要麼是16)
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 將新數組賦值給當前對象的數組屬性
table = newTab;
// 如果舊的數組不是null
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)
// 調用紅黑樹 的split 方法,傳入當前對象,新數組,當前下標,舊數組的容量,目的是將樹的數據重新散列到數組中
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 如果既不是樹,next 節點也不爲空,則是鏈表,注意,這裏將優化鏈表重新散列
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) {
// 銷燬實例,等待GC回收
loTail.next = null;
// 置入bucket中
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
總結:HashMap在擴容的時候resize 需要重建 hash 表,所以纔會影響性能。
1.爲什麼要使用構造函數指定HashMap的容量
避免多次擴容
2.如果不指定會對效率造成多大的影響
以放置 1024 個元素爲例,容量7次擴容,其中不光是七次重新計算HashCode,如果HashCode碰撞較多,還會涉及鏈表(鏈表中數據>=8,並且HashMap容量<64會進行重新散列,如果HashMap容量>64就會進行紅黑樹的轉換),以及紅黑樹的轉換等。擴容中我比較喜歡的地方在於重新重建hash 表後,原來鏈表中以及樹種的內容可能就不會因爲衝突導致以鏈表或者樹的形式存在!比較欣慰。
擴展話題:如果需要制定HashMap的容量那麼多少爲好呢?
源碼:
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
這個方法會將我們如數的值尋找最近的2的冪次方,如輸入10則會轉換爲16
我們知道了,無論我們如何設置初始容量,HashMap的tableSizeFor(int cap) 都會將我們改成2的冪次方,也就是說,HashMap 的容量百分之百是 2的冪次方,因爲HashMap 太依賴他了。但是,請注意:如果我們預計插入7條數據,那麼我們寫入7,HashMap 會設置爲 8,雖然是2的冪次方,但是,請注意,當我們放入第7條數據的時候,就會引起擴容,造成性能損失,所以,知曉了原理,我們以後在設置容量的時候還是自己算一下,比如放7條數據,我們還是都是設置成16,這樣就不會擴容了。
計算公式:
這裏就要說到手冊裏提到的 “注意負載因子(即loader factor)默認爲 0.75”
假如我們要插入7條數據,tableSizeFor(int cap)會將我們輸入的7運算成8。
我們使用 8* 0.75(負載因子)=6 也就是說最大閾值爲6條
當我們插入第七條的時候它就擴容了,所以我們最好在指定容量的時候多預算一些。
另:不光是HashMap需要指定大小,其他數據結構在知曉其存儲的數量時也應指定。
摘自《阿里開發手冊》
【推薦】任何數據結構的構造或初始化,都應指定大小,避免數據結構無限增長吃光內存。
————————————————
版權聲明:本文爲CSDN博主「七八月份的太陽」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/weixin_40165163/article/details/84402308