科技

為什麼阿里巴巴建議集合初始化時,指定集合容量大小?_HashMap

作者 | Hollis

本文經授權轉載自Hollis(ID:hollischuang)

關於集合類,《阿里巴巴Java開發手冊》中有一個規定:

本文就來分析一下為什麼會有如此建議?如果一定要設定初始容量的話,設定多少比較合適?

為什麼要設定初始容量

我們先來寫一段程式碼在JDK 1.7 (jdk1.7.0_79)下面來分別測試下,在不指定初始化容量和指定初始化容量的情況下效能情況如何。(jdk 8 結果會有所不同,我會在後面的文章中分析)

publicstaticvoidmain(String[]args){

intaHundredMillion =10000000;

MapInteger,Integer>map =newHashMap();

longs1 =System.currentTimeMillis();

for(inti =0;i aHundredMillion;i++)

longs2 =System.currentTimeMillis();

System.out.println("未初始化容量,耗時 :"+(s2 -s1));

MapInteger,Integer>map1 =newHashMap(aHundredMillion /2);

longs5 =System.currentTimeMillis();

for(inti =0;i aHundredMillion;i++)

longs6 =System.currentTimeMillis();

System.out.println("初始化容量5000000,耗時 :"+(s6 -s5));

MapInteger,Integer>map2 =newHashMap(aHundredMillion);

longs3 =System.currentTimeMillis();

for(inti =0;i aHundredMillion;i++)

longs4 =System.currentTimeMillis();

System.out.println("初始化容量為10000000,耗時 :"+(s4 -s3));

}

以上程式碼不難理解,我們建立了3個HashMap,分別使用預設的容量(16)、使用元素個數的一半(5千萬)作為初始容量、使用元素個數(一億)作為初始容量進行初始化。然後分別向其中put一億個KV。

輸出結果:

未初始化容量,耗時:14419

初始化容量5000000,耗時:11916

初始化容量為10000000,耗時:7984

從結果中,我們可以知道,在已知HashMap中將要存放的KV個數的時候,設定一個合理的初始化容量可以有效的提高效能。

當然,以上結論也是有理論支撐的。我們HashMap中傻傻分不清楚的那些概念文章介紹過,HashMap有擴容機制,就是當達到擴容條件時會進行擴容。HashMap的擴容條件就是當HashMap中的元素個數(size)超過臨界值(threshold)時就會自動擴容。在HashMap中, threshold = loadFactor * capacity。

所以,如果我們沒有設定初始容量大小,隨著元素的不斷增加,HashMap會發生多次擴容,而HashMap中的擴容機制決定了每次擴容都需要重建hash表,是非常影響效能的。

從上面的程式碼示例中,我們還發現,同樣是設定初始化容量,設定的數值不同也會影響效能,那麼當我們已知HashMap中即將存放的KV個數的時候,容量設定成多少為好呢?

HashMap中容量的初始化

預設情況下,當我們設定HashMap的初始化容量時,實際上HashMap會採用第一個大於該數值的2的冪作為初始化容量。

如以下示例程式碼:

MapString,String>map =newHashMapString,String>(1);

map.put("hahaha","hollischuang");

Class>mapType =map.getClass();

Methodcapacity =mapType.getDeclaredMethod("capacity");

capacity.setAccessible(true);

System.out.println("capacity : "+capacity.invoke(map));

在jdk1.7中,初始化容量設定成1的時候,輸出結果是2。在jdk1.8中,如果我們傳入的初始化容量為1,實際上設定的結果也為1,上面程式碼輸出結果為2的原因是程式碼中map.put(“hahaha”, “hollischuang”);導致了擴容,容量從1擴容到2。

那麼,話題再說回來,當我們通過HashMap(int initialCapacity)設定初始容量的時候,HashMap並不一定會直接採用我們傳入的數值,而是經過計算,得到一個新值,目的是提高hash的效率。(1->1、3->4、7->8、9->16)

在Jdk 1.7和Jdk 1.8中,HashMap初始化這個容量的時機不同。jdk1.8中,在呼叫HashMap的建構函式定義HashMap的時候,就會進行容量的設定。而在Jdk 1.7中,要等到第一次put操作時才進行這一操作。

不管是Jdk 1.7還是Jdk 1.8,計算初始化容量的演算法其實是如出一轍的,主要程式碼如下:

intn =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;

上面的程式碼挺有意思的,一個簡單的容量初始化,Java的工程師也有很多考慮在裡面。

上面的演算法目的挺簡單,就是:根據使用者傳入的容量值(程式碼中的cap),通過計算,得到第一個比他大的2的冪並返回。

聰明的讀者們,如果讓你設計這個演算法你準備如何計算?如果你想到二進位制的話,那就很簡單了。舉幾個例子看一下:

請關注上面的幾個例子中,藍色字型部分的變化情況,或許你會發現些規律。5->8、9->16、19->32、37->64都是主要經過了兩個階段。

Step 1,5->7

Step 2,7->8

Step 1,9->15

Step 2,15->16

Step 1,19->31

Step 2,31->32

Step 1,37->63

Step 2,63->65

對應到以上程式碼中,Step 1:

n |=n >>>1;

n |=n >>>2;

n |=n >>>4;

n |=n >>>8;

n |=n >>>16;

對應到以上程式碼中,Step2:

return(n 0)?1:(n >=MAXIMUM_CAPACITY)?MAXIMUM_CAPACITY :n + 1;

Step 2 比較簡單,就是做一下極限值的判斷,然後把Step 1得到的數值+1。

Step 1 怎麼理解呢?其實是對一個二進位制數依次向右移位,然後與原值取或。其目的對於一個數字的二進位制,從第一個不為0的位開始,把後面的所有位都設定成1。

隨便拿一個二進位制數,套一遍上面的公式就發現其目的了:

110011001100>>>1=011001100110

110011001100|011001100110=111011101110

111011101110>>>2=001110111011

111011101110|001110111011=111111111111

111111111111>>>4=111111111111

111111111111|111111111111=111111111111

通過幾次無符號右移和按位或運算,我們把1100 1100 1100轉換成了1111 1111 1111 ,再把1111 1111 1111加1,就得到了1 0000 0000 0000,這就是大於1100 1100 1100的第一個2的冪。

好了,我們現在解釋清楚了Step 1和Step 2的程式碼。就是可以把一個數轉化成第一個比他自身大的2的冪。(可以開始佩服Java的工程師們了,使用無符號右移和按位或運算大大提升了效率。)

但是還有一種特殊情況套用以上公式不行,這些數字就是2的冪自身。如果數字4 套用公式的話。得到的會是 8 :

Step1:

0100>>>1=0010

0100|0010=0110

0110>>>1=0011

0110|0011=0111

Step2:

0111+0001=1000

為了解決這個問題,JDK的工程師把所有使用者傳進來的數在進行計算之前先-1,就是原始碼中的第一行:

intn =cap -1;

至此,再來回過頭看看這個設定初始容量的程式碼,目的是不是一目瞭然了:

intn =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;

HashMap初始容量的合理值

當我們使用 HashMap(int initialCapacity) 來初始化容量的時候,jdk會預設幫我們計算一個相對合理的值當做初始容量。

那麼,是不是我們只需要把已知的HashMap中即將存放的元素個數直接傳給initialCapacity就可以了呢?

關於這個值的設定,在《阿里巴巴Java開發手冊》有以下建議:

這個值,並不是阿里巴巴的工程師原創的,在guava(21.0版本)中也使用的是這個值。

publicstaticK,V>HashMapK,V>newHashMapWithExpectedSize(intexpectedSize)

/**

* Returns a capacity that is sufficient to keep the map from being resized as long as it grows no

* larger than expectedSize and the load factor is ≥ its default (0.75).

*/

staticintcapacity(intexpectedSize){

if(expectedSize 3){

checkNonnegative(expectedSize,"expectedSize");

returnexpectedSize +1;

}

if(expectedSize Ints.MAX_POWER_OF_TWO){

// This is the calculation used in JDK8 to resize when a putAll

// happens; it seems to be the most conservative calculation we

// can make. 0.75 is the default load factor.

return(int)((float)expectedSize /0.75F+1.0F);

}

returnInteger.MAX_VALUE;// any large value

}

在return (int) ((float) expectedSize / 0.75F + 1.0F);上面有一行註釋,說明了這個公式也不是guava原創,參考的是JDK8中putAll方法中的實現的。

感興趣的讀者可以去看下putAll方法的實現,也是以上的這個公式。

雖然,當我們使用 HashMap(int initialCapacity) 來初始化容量的時候,jdk會預設幫我們計算一個相對合理的值當做初始容量。但是這個值並沒有參考loadFactor的值。

也就是說,如果我們設定的預設值是7,經過Jdk處理之後,會被設定成8,但是,這個HashMap在元素個數達到 8*0.75 = 6的時候就會進行一次擴容,這明顯是我們不希望見到的。

如果我們通過 expectedSize / 0.75F + 1.0F 計算,7/0.75 + 1 = 10 ,10經過Jdk處理之後,會被設定成16,這就大大的減少了擴容的機率。

當HashMap內部維護的雜湊表的容量達到75%時(預設情況下),會觸發rehash,而rehash的過程是比較耗費時間的。

所以初始化容量要設定成expectedSize/0.75 + 1的話,可以有效的減少衝突也可以減小誤差。

所以,我可以認為,當我們明確知道HashMap中元素的個數的時候,把預設容量設定成 expectedSize / 0.75F + 1.0F 是一個在效能上相對好的選擇,但是,同時也會犧牲些記憶體。

總結

當我們想要在程式碼中建立一個HashMap的時候,如果我們已知這個Map中即將存放的元素個數,給HashMap設定初始容量可以在一定程度上提升效率。

但是,JDK並不會直接拿使用者傳進來的數字當做預設容量,而是會進行一番運算,最終得到一個2的冪。

原因在《全網把Map中的hash()分析的最透徹的文章,別無二家》介紹過,得到這個數字的演算法其實是使用了使用無符號右移和按位或運算來提升效率。

但是,為了最大程度的避免擴容帶來的效能消耗,我們建議可以把預設容量的數字設定成expectedSize / 0.75F + 1.0F 。在日常開發中,可以使用

MapString,String>map =Maps.newHashMapWithExpectedSize(10);

來建立一個HashMap,計算的過程guava會幫我們完成。

但是,以上的操作是一種用記憶體換效能的做法,真正使用的時候,要考慮到記憶體的影響。

最後,留一個思考題:為什麼JDK 8中,putAll方法採用了這個expectedSize / 0.75F + 1.0F公式,而put、建構函式等並沒有預設使用這個公式呢?

作者簡介:Hollis,知名技術博主,個人部落格文章閱讀量數百萬,CSDN部落格專家。工作地點杭州,主要從事金融,支付領域的Java開發,熱衷於技術分享。

本文轉載自Hollis公眾號,專注原創技術文章,主要以Java相關技術為主,覆蓋基礎知識、底層原理、技術成長等話題。

【End】

責任編輯:

Reference:科技日報

看更多!請加入我們的粉絲團

轉載請附文章網址

不可錯過的話題