日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

关于HashMap容量的初始化,还有这么多学问

發(fā)布時(shí)間:2023/12/3 编程问答 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 关于HashMap容量的初始化,还有这么多学问 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

轉(zhuǎn)載自?關(guān)于HashMap容量的初始化,還有這么多學(xué)問

在《HashMap中傻傻分不清楚的那些概念》文章中,我們介紹了HashMap中和容量相關(guān)的幾個(gè)概念,簡(jiǎn)單介紹了一下HashMap的擴(kuò)容機(jī)制。


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


本文,延續(xù)上一篇文章,我們?cè)賮砩钊雽W(xué)習(xí)下,到底應(yīng)不應(yīng)該設(shè)置HashMap的默認(rèn)容量?如果真的要設(shè)置HashMap的初始容量,我們應(yīng)該設(shè)置多少?

為什么要設(shè)置HashMap的初始化容量

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

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

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

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("未初始化容量,耗時(shí) : "?+ (s2 - s1));


? ?Map<Integer, Integer> map1 =?new?HashMap<>(aHundredMillion /?2);

? ?long?s5 = System.currentTimeMillis();
? ?for?(int?i =?0; i < aHundredMillion; i++) {
? ? ? ?map1.put(i, i);
? ?}
? ?long?s6 = System.currentTimeMillis();

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


? ?Map<Integer, Integer> map2 =?new?HashMap<>(aHundredMillion);

? ?long?s3 = System.currentTimeMillis();
? ?for?(int?i =?0; i < aHundredMillion; i++) {
? ? ? ?map2.put(i, i);
? ?}
? ?long?s4 = System.currentTimeMillis();

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

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

輸出結(jié)果:

未初始化容量,耗時(shí) : 14419
初始化容量5000000,耗時(shí) : 11916
初始化容量為10000000,耗時(shí) : 7984

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

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

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

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

HashMap中容量的初始化

在上一篇文章中,我們通過代碼實(shí)例其實(shí)介紹過,默認(rèn)情況下,當(dāng)我們?cè)O(shè)置HashMap的初始化容量時(shí),實(shí)際上HashMap會(huì)采用第一個(gè)大于該數(shù)值的2的冪作為初始化容量。

上一篇文章中有個(gè)例子

Map<String, String>?map?=?new?HashMap<String, String>(1);
map.put("hahaha",?"hollischuang");

Class<?> mapType =?map.getClass();
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : "?+ capacity.invoke(map));

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

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

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

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

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;

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

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

聰明的讀者們,如果讓你設(shè)計(jì)這個(gè)算法你準(zhǔn)備如何計(jì)算?如果你想到二進(jìn)制的話,那就很簡(jiǎn)單了。舉幾個(gè)例子看一下:

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

Step 1,5->7

Step 2,7->8


Step 1,9->15

Step 2,15->16


Step 1,19->31

Step 2,31->32

對(duì)應(yīng)到以上代碼中,Step1:

n?|= n >>>?1;
n?|= n >>>?2;
n?|= n >>>?4;
n?|= n >>>?8;
n?|= n >>>?16;

對(duì)應(yīng)到以上代碼中,Step2:

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

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

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

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

1100?1100?1100?>>>1?=?0110?0110?0110
1100?1100?1100?| 0110 0110 0110 = 1110 1110 1110
1110 1110 1110 >>>2 = 0011 1011 1011
1110 1110 1110 |
?0011?1011?1011?=?1111?1111?1111
1111?1111?1111?>>>4?=?1111?1111?1111
1111?1111?1111?| 1111 1111 1111 = 1111 1111 1111

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

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

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

Step?1:?
0100?>>>1?=?0010
0100?| 0010 = 0110
0110 >>>1 = 0011
0110 |
?0011?=?0111


Step?2:
0111?+?0001?=?1000

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

int?n =?cap?-?1;

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

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;HashMap中初始容量的合理值

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

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

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

public?static?<K, V>?HashMap<K, V>?newHashMapWithExpectedSize(int?expectedSize)?{
? ?return?new?HashMap<K, V>(capacity(expectedSize));
}

/**
* 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).
*/

static?int?capacity(int?expectedSize)?{
? ?if?(expectedSize <?3) {
? ? ?checkNonnegative(expectedSize,?"expectedSize");
? ? ?return?expectedSize +?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);
? ?}
? ?return?Integer.MAX_VALUE;?// any large value
}

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

雖然,當(dāng)我們使用HashMap(int initialCapacity)來初始化容量的時(shí)候,jdk會(huì)默認(rèn)幫我們計(jì)算一個(gè)相對(duì)合理的值當(dāng)做初始容量。但是這個(gè)值并沒有參考loadFactor的值。

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

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

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

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

總結(jié)

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

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

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

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

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

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

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


創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)

總結(jié)

以上是生活随笔為你收集整理的关于HashMap容量的初始化,还有这么多学问的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。