自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

關于HashMap容量的初始化,還有這么多學問

企業(yè)動態(tài)
我們再來深入學習下,到底應不應該設置HashMap的默認容量?如果真的要設置HashMap的初始容量,我們應該設置多少?

《HashMap中傻傻分不清楚的那些概念》文章中,我們介紹了HashMap中和容量相關的幾個概念,簡單介紹了一下HashMap的擴容機制。

[[230879]]

文中我們提到,默認情況下HashMap的容量是16,但是,如果用戶通過構(gòu)造函數(shù)指定了一個數(shù)字作為容量,那么Hash會選擇大于該數(shù)字的***個2的冪作為容量。(3->4、7->8、9->16)

本文,延續(xù)上一篇文章,我們再來深入學習下,到底應不應該設置HashMap的默認容量?如果真的要設置HashMap的初始容量,我們應該設置多少?

為什么要設置HashMap的初始化容量

我們之前提到過,《阿里巴巴Java開發(fā)手冊》中建議我們設置HashMap的初始化容量。

那么,為什么要這么建議?你有想過沒有。

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

  1. public static void main(String[] args) { 
  2.    int aHundredMillion = 10000000; 
  3.  
  4.    Map<IntegerInteger> map = new HashMap<>(); 
  5.  
  6.    long s1 = System.currentTimeMillis(); 
  7.    for (int i = 0; i < aHundredMillion; i++) { 
  8.        map.put(i, i); 
  9.    } 
  10.    long s2 = System.currentTimeMillis(); 
  11.  
  12.    System.out.println("未初始化容量,耗時 : " + (s2 - s1)); 
  13.  
  14.  
  15.    Map<IntegerInteger> map1 = new HashMap<>(aHundredMillion / 2); 
  16.  
  17.    long s5 = System.currentTimeMillis(); 
  18.    for (int i = 0; i < aHundredMillion; i++) { 
  19.        map1.put(i, i); 
  20.    } 
  21.    long s6 = System.currentTimeMillis(); 
  22.  
  23.    System.out.println("初始化容量5000000,耗時 : " + (s6 - s5)); 
  24.  
  25.  
  26.    Map<IntegerInteger> map2 = new HashMap<>(aHundredMillion); 
  27.  
  28.    long s3 = System.currentTimeMillis(); 
  29.    for (int i = 0; i < aHundredMillion; i++) { 
  30.        map2.put(i, i); 
  31.    } 
  32.    long s4 = System.currentTimeMillis(); 
  33.  
  34.    System.out.println("初始化容量為10000000,耗時 : " + (s4 - s3)); 

以上代碼不難理解,我們創(chuàng)建了3個HashMap,分別使用默認的容量(16)、使用元素個數(shù)的一半(5千萬)作為初始容量、使用元素個數(shù)(一億)作為初始容量進行初始化。然后分別向其中put一億個KV。

輸出結(jié)果:

  1. 未初始化容量,耗時 : 14419 
  2. 初始化容量5000000,耗時 : 11916 
  3. 初始化容量為10000000,耗時 : 7984 

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

當然,以上結(jié)論也是有理論支撐的。我們上一篇文章介紹過,HashMap有擴容機制,就是當達到擴容條件時會進行擴容。HashMap的擴容條件就是當HashMap中的元素個數(shù)(size)超過臨界值(threshold)時就會自動擴容。在HashMap中,threshold = loadFactor * capacity。

所以,如果我們沒有設置初始容量大小,隨著元素的不斷增加,HashMap會發(fā)生多次擴容,而HashMap中的擴容機制決定了每次擴容都需要重建hash表,是非常影響性能的。(關于resize,后面我會有文章單獨介紹)

從上面的代碼示例中,我們還發(fā)現(xiàn),同樣是設置初始化容量,設置的數(shù)值不同也會影響性能,那么當我們已知HashMap中即將存放的KV個數(shù)的時候,容量設置成多少為好呢?

HashMap中容量的初始化

在上一篇文章中,我們通過代碼實例其實介紹過,默認情況下,當我們設置HashMap的初始化容量時,實際上HashMap會采用***個大于該數(shù)值的2的冪作為初始化容量。

上一篇文章中有個例子

  1. Map<String, String> map = new HashMap<String, String>(1); 
  2. map.put("hahaha""hollischuang"); 
  3.  
  4. Class<?> mapType = map.getClass(); 
  5. Method capacity = mapType.getDeclaredMethod("capacity"); 
  6. capacity.setAccessible(true); 
  7. System.out.println("capacity : " + capacity.invoke(map)); 

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

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

在Jdk 1.7和Jdk 1.8中,HashMap初始化這個容量的時機不同。jdk1.8中,在調(diào)用HashMap的構(gòu)造函數(shù)定義HashMap的時候,就會進行容量的設定。而在Jdk 1.7中,要等到***次put操作時才進行這一操作。

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

  1. int n = cap - 1; 
  2. n |= n >>> 1; 
  3. n |= n >>> 2; 
  4. n |= n >>> 4; 
  5. n |= n >>> 8; 
  6. n |= n >>> 16; 
  7. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 

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

上面的算法目的挺簡單,就是:根據(jù)用戶傳入的容量值(代碼中的cap),通過計算,得到***個比他大的2的冪并返回。

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

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

  • Step 1,5->7
  • Step 2,7->8
  • Step 1,9->15
  • Step 2,15->16
  • Step 1,19->31
  • Step 2,31->32

對應到以上代碼中,Step1:

  1. n |= n >>> 1;  
  2. n |= n >>> 2;  
  3. n |= n >>> 4; 
  4. n |= n >>> 8;  
  5. n |= n >>> 16; 

對應到以上代碼中,Step2:

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

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

Step 1 怎么理解呢?其實是對一個二進制數(shù)依次向右移位,然后與原值取或。其目的對于一個數(shù)字的二進制,從***個不為0的位開始,把后面的所有位都設置成1。

隨便拿一個二進制數(shù),套一遍上面的公式就發(fā)現(xiàn)其目的了:

  1. 1100 1100 1100 >>>1 = 0110 0110 0110 
  2. 1100 1100 1100 | 0110 0110 0110 = 1110 1110 1110 
  3. 1110 1110 1110 >>>2 = 0011 1011 1011 
  4. 1110 1110 1110 | 0011 1011 1011 = 1111 1111 1111 
  5. 1111 1111 1111 >>>4 = 1111 1111 1111 
  6. 1111 1111 1111 | 1111 1111 1111 = 1111 1111 1111 

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

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

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

  1. Step 1:  
  2. 0100 >>>1 = 0010 
  3. 0100 | 0010 = 0110 
  4. 0110 >>>1 = 0011 
  5. 0110 | 0011 = 0111 
  6.  
  7.  
  8. Step 2: 
  9. 0111 + 0001 = 1000 

為了解決這個問題,JDK的工程師把所有用戶傳進來的數(shù)在進行計算之前先-1,就是源碼中的***行:

  1. int n = cap - 1; 

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

  1. int n = cap - 1; 
  2. n |= n >>> 1; 
  3. n |= n >>> 2; 
  4. n |= n >>> 4; 
  5. n |= n >>> 8; 
  6. n |= n >>> 16; 
  7. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; 

HashMap中初始容量的合理值

當我們使用HashMap(int initialCapacity)來初始化容量的時候,jdk會默認幫我們計算一個相對合理的值當做初始容量。那么,是不是我們只需要把已知的HashMap中即將存放的元素個數(shù)直接傳給initialCapacity就可以了呢?

關于這個值的設置,在《阿里巴巴Java開發(fā)手冊》有以下建議:

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

  1. public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) { 
  2.    return new HashMap<K, V>(capacity(expectedSize)); 
  3.  
  4. /** 
  5. Returns a capacity that is sufficient to keep the map from being resized as long as it grows no 
  6. * larger than expectedSize and the load factor is ≥ its default (0.75). 
  7. */ 
  8. static int capacity(int expectedSize) { 
  9.    if (expectedSize < 3) { 
  10.      checkNonnegative(expectedSize, "expectedSize"); 
  11.      return expectedSize + 1; 
  12.    } 
  13.    if (expectedSize < Ints.MAX_POWER_OF_TWO) { 
  14.      // This is the calculation used in JDK8 to resize when a putAll 
  15.      // happens; it seems to be the most conservative calculation we 
  16.      // can make.  0.75 is the default load factor. 
  17.      return (int) ((float) expectedSize / 0.75F + 1.0F); 
  18.    } 
  19.    return Integer.MAX_VALUE; // any large value 

在return (int) ((float) expectedSize / 0.75F + 1.0F);上面有一行注釋,說明了這個公式也不是guava原創(chuàng),參考的是JDK8中putAll方法中的實現(xiàn)的。感興趣的讀者可以去看下putAll方法的實現(xiàn),也是以上的這個公式。

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

也就是說,如果我們設置的默認值是7,經(jīng)過Jdk處理之后,會被設置成8,但是,這個HashMap在元素個數(shù)達到 8*0.75 = 6的時候就會進行一次擴容,這明顯是我們不希望見到的。

如果我們通過expectedSize / 0.75F + 1.0F計算,7/0.75 + 1 = 10 ,10經(jīng)過Jdk處理之后,會被設置成16,這就大大的減少了擴容的幾率。

當HashMap內(nèi)部維護的哈希表的容量達到75%時(默認情況下),會觸發(fā)rehash,而rehash的過程是比較耗費時間的。所以初始化容量要設置成expectedSize/0.75 + 1的話,可以有效的減少沖突也可以減小誤差。

所以,我可以認為,當我們明確知道HashMap中元素的個數(shù)的時候,把默認容量設置成expectedSize / 0.75F + 1.0F 是一個在性能上相對好的選擇,但是,同時也會犧牲些內(nèi)存。

總結(jié)

當我們想要在代碼中創(chuàng)建一個HashMap的時候,如果我們已知這個Map中即將存放的元素個數(shù),給HashMap設置初始容量可以在一定程度上提升效率。

但是,JDK并不會直接拿用戶傳進來的數(shù)字當做默認容量,而是會進行一番運算,最終得到一個2的冪。原因在(全網(wǎng)把Map中的hash()分析的最透徹的文章,別無二家。)介紹過,得到這個數(shù)字的算法其實是使用了使用無符號右移和按位或運算來提升效率。

但是,為了***程度的避免擴容帶來的性能消耗,我們建議可以把默認容量的數(shù)字設置成expectedSize / 0.75F + 1.0F 。在日常開發(fā)中,可以使用

  1. Map<String, String> map = Maps.newHashMapWithExpectedSize(10); 

來創(chuàng)建一個HashMap,計算的過程guava會幫我們完成。

但是,以上的操作是一種用內(nèi)存換性能的做法,真正使用的時候,要考慮到內(nèi)存的影響。

***,留一個思考題:為什么JDK 8中,putAll方法采用了這個expectedSize / 0.75F + 1.0F公式,而put、構(gòu)造函數(shù)等并沒有默認使用這個公式呢?

【本文是51CTO專欄作者Hollis的原創(chuàng)文章,作者微信公眾號Hollis(ID:hollischuang)】

戳這里,看該作者更多好文

責任編輯:武曉燕 來源: 51CTO專欄
相關推薦

2021-01-14 05:08:44

編譯鏈接

2017-07-04 14:01:40

機房機柜

2024-02-20 08:09:51

Java 8DateUtilsDate工具類

2023-11-13 08:49:54

2017-06-16 16:16:36

庫存扣減查詢

2024-01-31 12:34:16

panic錯誤檢測recover

2018-06-26 15:00:24

Docker安全風險

2019-12-09 10:13:20

HashMap選擇容量

2022-01-12 20:04:09

網(wǎng)絡故障斷網(wǎng)事件網(wǎng)絡安全

2018-01-31 16:12:47

筆記本輕薄本游戲本

2022-05-29 08:54:44

Edge瀏覽器

2024-01-02 12:48:49

2013-01-15 09:41:45

編程語言

2017-12-21 19:38:50

潤乾中間表

2022-07-26 23:43:29

編程語言開發(fā)Java

2021-05-31 22:26:20

5G技術通信

2020-06-16 14:11:48

find命令文件查找

2024-05-13 16:22:25

固態(tài)硬盤接口硬盤

2017-07-12 08:20:32

閃存用途企業(yè)

2021-01-05 07:00:53

微信隱藏功能移動應用
點贊
收藏

51CTO技術棧公眾號