HashMap使用HashMap(int initialCapacity)對集合進行初始化。算法
在默認的狀況下,HashMap的容量是16。可是若是用戶經過構造函數指定了一個數字做爲容量,那麼Hash會選擇大於該數字的第一個2的冪做爲容量。好比若是指定了3,則容量是4;若是指定了7,則容量是8;若是指定了9,則容量是16。函數
爲何要設置HashMap的初始化容量性能
在《阿里巴巴Java開發手冊》中,有一條開發建議是建議咱們設置HashMap的初始化容量。測試
下面咱們經過具體的代碼來了解下爲何會這麼建議。spa
咱們先來寫一段代碼在JDK1.7的環境下運行,來分別測試下,在不指定初始化容量和指定初始化容量的狀況下性能狀況的不一樣。code
public static void main(String[] args) { int aHundredMillion = 10000000; // 未初始化容量 Map<Integer, Integer> map = new HashMap<>(); long s1 = System.currentTimeMillis(); for (int i = 0; i < aHundredMillion; i++) { map.put(i, i); } long s2 = System.currentTimeMillis(); System.out.println("未初始化容量,耗時: " + (s2 - s1)); // 14322 // 初始化容量爲50000000 Map<Integer, Integer> map1 = new HashMap<>(aHundredMillion / 2); long s3 = System.currentTimeMillis(); for (int i = 0; i < aHundredMillion; i++) { map1.put(i, i); } long s4 = System.currentTimeMillis(); System.out.println("初始化容量5000000,耗時: " + (s4 - s3)); // 11819 // 初始化容量爲100000000 Map<Integer, Integer> map2 = new HashMap<>(aHundredMillion); long s5 = System.currentTimeMillis(); for (int i = 0; i < aHundredMillion; i++) { map2.put(i, i); } long s6 = System.currentTimeMillis(); System.out.println("初始化容量爲10000000,耗時: " + (s6 - s5)); // 7978 }
從以上的代碼不難理解,咱們建立了3個HashMap,分別使用默認的容量(16)、使用元素個數的一半(5千萬)做爲初始容量和使用元素個數(一億)做爲初始容量進行初始化,而後分別向其中put一億個KV。blog
從上面的打印結果中能夠獲得一個初步的結論:在已知HashMap中將要存放的KV個數的時候,設置一個合理的初始化容量能夠有效地提升性能。下面咱們來簡單分析一下緣由。內存
咱們知道,HashMap是有擴容機制的。所謂的擴容機制,指的是當達到擴容條件的時候,HashMap就會自動進行擴容。而HashMap的擴容條件就是當HashMap中的元素個數(Size)超過臨界值(Threshold)的狀況下就會自動擴容。ci
threshold = loadFactor * capacity
在元素個數超過臨界值的狀況下,隨着元素的不斷增長,HashMap就會發生擴容,而HashMap中的擴容機制決定了每次擴容都須要重建hash表,這一操做須要消耗大量資源,是很是影響性能的。所以,若是咱們沒有設置初始的容量大小,HashMap就可能會不斷髮生擴容,也就使得程序的性能下降了。資源
另外,在上面的代碼中咱們會發現,一樣是設置了初始化容量,設置的數值不一樣也會影響性能,那麼當咱們已知HashMap中即將存放的KV個數的時候,容量的設置就成了一個問題。
HashMap中容量的初始化
開頭提到,在默認的狀況下,當咱們設置HashMap的初始化容量時,實際上HashMap會採用第一個大於該數值的2的冪做爲初始化容量。
Map<String, String> map = new HashMap<>(1); map.put("huangq", "yanggb"); Class<?> mapType = map.getClass(); Method capacity = mapType.getDeclaredMethod("capacity"); capacity.setAccessible(true); System.out.println("capacity : " + capacity.invoke(map)); // 2
當初始化的容量設置成1的時候,經過反射取出來的capacity倒是2。在JDK1.8中,若是咱們傳入的初始化容量爲1,實際上設置的結果也是1。上面的代碼打印的結果爲2的緣由,是代碼中給map塞入值的操做致使了擴容,容量從1擴容到了2。事實上,在JDK1.7和JDK1.8中,HashMap初始化容量(capacity)的時機不一樣。在JDK1.8中,調用HashMap的構造函數定義HashMap的時候,就會進行容量的設定。而在JDK1.7中,要等到第一次put操做時才進行這一操做。
所以,當咱們經過HashMap(int initialCapacity)設置初始容量的時候,HashMap並不必定會直接採用咱們傳入的數值,而是通過計算,獲得一個新值,目的是提升hash的效率。好比1->一、3->四、7->8和9->16。
HashMap中初始容量的合理值
經過上面的分析咱們能夠知道,當咱們使用HashMap(int initialCapacity)來初始化容量的時候,JDK會默認幫咱們計算一個相對合理的值當作初始容量。那麼,是否是咱們只須要把已知的HashMap中即將存放的元素個數直接傳給initialCapacity就能夠了呢?
initialCapacity = (須要存儲的元素個數 / 負載因子) + 1
這裏的負載因子就是loaderFactor,默認值爲0.75。
initialCapacity = expectedSize / 0.75F + 1.0F
上面這個公式是《阿里巴巴Java開發手冊》中的一個建議,在Guava中也是提供了相同的算法,更甚之,這個算法其實是JDK8中putAll()方法的實現。這是公式的得出是由於,當HashMap內部維護的哈希表的容量達到75%時(默認狀況下),就會觸發rehash(重建hash表)操做。而rehash的過程是比較耗費時間的。因此初始化容量要設置成expectedSize/0.75 + 1的話,能夠有效地減小衝突,也能夠減少偏差。
總結
當咱們想要在代碼中建立一個HashMap的時候,若是咱們已知這個Map中即將存放的元素個數,給HashMap設置初始容量能夠在必定程度上提高效率。
可是,JDK並不會直接拿用戶傳進來的數字當作默認容量,而是會進行一番運算,最終獲得一個2的冪。而爲了最大程度地避免擴容帶來的性能消耗,一般是建議能夠把默認容量的數字設置成expectedSize / 0.75F + 1.0F。
在平常開發中,可使用Guava提供的一個方法來建立一個HashMap,計算的過程Guava會幫咱們完成。
Map<String, String> map = Maps.newHashMapWithExpectedSize(10);
最後要說的一點是,這種算法其實是一種使用內存換取性能的作法,在真正的應用場景中要考慮到內存的影響。
"當你認真喜歡一我的的時候,你的全世界都是她。"