为什么HashMap默认初始容量为2次幂?不是2次幂会怎样?讲讲 HashMap 扰动函数?
關(guān)于HashMap的詳解文章請(qǐng)移步:
鏈接: HashMap源碼研究——源碼一行一行的注釋
文章目錄
- 為什么初始容量是 2次冪?
- 如果指定了不是2的次冪的容量會(huì)發(fā)生什么?
- 有一個(gè)初始容量參數(shù)的構(gòu)造方法HashMap(int initialCapacity)
- 有兩個(gè)參數(shù)的構(gòu)造方法HashMap(int initialCapacity, float loadFactor)
- 擾動(dòng)函數(shù)
為什么初始容量是 2次冪?
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 如果沒(méi)有hash碰撞則直接插入元素if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);else {......} }通過(guò)看源碼,我們發(fā)現(xiàn),判斷桶的索引的實(shí)現(xiàn)是 i = ( n - 1 ) & hash,其中 n 是 map 的容量。
任何 2 的整數(shù)冪 - 1 得到的二進(jìn)制都是 1,如:16 - 1 = 15(1111);32 - 1 = 31(11111)
而 n-1 與 hash 做的是與運(yùn)算(&),與運(yùn)算是 兩個(gè)都為1,才為1
既然我們的 n-1 永遠(yuǎn)都是 1,那 ( n - 1 ) & hash 的計(jì)算結(jié)果就是 低位的hash 值。如:
00100100 10100101 11000100 00100101 // Hash 值 & 00000000 00000000 00000000 00001111 // 16 - 1 = 15 ----------------------------------00000000 00000000 00000000 00000101 // 高位全部歸零,只保留末四位。那容量不是 2次冪會(huì)怎么樣?我們來(lái)做個(gè)試驗(yàn)。
2次冪的情況:
非2次冪的情況,假設(shè) n = 10:
對(duì)比來(lái)看,哪種發(fā)生哈希碰撞的概率更低一目了然,如果 n 為 2次冪,可以保證數(shù)據(jù)的均勻插入,降低哈希沖突的概率,畢竟沖突越大,代表數(shù)組中的鏈表/紅黑樹(shù)越大,從而降低Hashmap 的性能。
如果指定了不是2的次冪的容量會(huì)發(fā)生什么?
答案:會(huì)獲得最接近的一個(gè)2的次冪作為容量
有一個(gè)初始容量參數(shù)的構(gòu)造方法HashMap(int initialCapacity)
參數(shù):initialCapacity 初始容量
public HashMap(int initialCapacity) {//此處通過(guò)把第二個(gè)參數(shù)負(fù)載因子使用默認(rèn)值0.75f,然后調(diào)用有兩個(gè)參數(shù)的構(gòu)造方法this(initialCapacity, DEFAULT_LOAD_FACTOR);}這個(gè)一個(gè)參數(shù)的構(gòu)造方法,使用HashMap的默認(rèn)負(fù)載因子,把該初始容量和默認(rèn)負(fù)載因子作為入?yún)?#xff0c;調(diào)用HashMap的兩個(gè)參數(shù)的構(gòu)造方法
有兩個(gè)參數(shù)的構(gòu)造方法HashMap(int initialCapacity, float loadFactor)
參數(shù):initialCapacity 初始容量
參數(shù):loadFactor 負(fù)載因子
我們下面看看tableSizeFor()這個(gè)方法是如何計(jì)算的,這個(gè)方法的實(shí)現(xiàn)原理很巧妙,源碼如下:
/*** Returns a power of two size for the given target capacity.*/static final int tableSizeFor(int cap) {int n = cap - 1; //容量減1,為了防止初始化容量已經(jīng)是2的冪的情況,最后有+1運(yùn)算。n |= n >>> 1; //將n無(wú)符號(hào)右移一位再與n做或操作n |= n >>> 2; //將n無(wú)符號(hào)右移兩位再與n做或操作n |= n >>> 4; //將n無(wú)符號(hào)右移四位再與n做或操作n |= n >>> 8; //將n無(wú)符號(hào)右移八位再與n做或操作n |= n >>> 16; //將n無(wú)符號(hào)右移十六位再與n做或操作//如果入?yún)ap為小于或等于0的數(shù),那么經(jīng)過(guò)cap-1之后n為負(fù)數(shù),n經(jīng)過(guò)無(wú)符號(hào)右移和或操作后仍未負(fù) //數(shù),所以如果n<0,則返回1;如果n大于或等于最大容量,則返回最大容量;否則返回n+1return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}首先,為什么要對(duì)cap做減1操作。int n = cap - 1;
這是為了防止,cap已經(jīng)是2的冪。如果cap已經(jīng)是2的冪, 又沒(méi)有執(zhí)行這個(gè)減1操作,則執(zhí)行完后面的幾條無(wú)符號(hào)右移操作之后,返回的capacity將是這個(gè)cap的2倍。如果不懂,要看完后面的幾個(gè)無(wú)符號(hào)右移之后再回來(lái)看看。
下面看看這幾個(gè)無(wú)符號(hào)右移操作:
如果n這時(shí)為0了(經(jīng)過(guò)了cap-1之后),則經(jīng)過(guò)后面的幾次無(wú)符號(hào)右移依然是0,最后返回的capacity是1(最后有個(gè)n+1的操作)。
這里只討論n不等于0的情況。
第一次右移
n |= n >> 1;
由于n不等于0,則n的二進(jìn)制表示中總會(huì)有一bit為1,這時(shí)考慮最高位的1。通過(guò)無(wú)符號(hào)右移1位,則將最高位的1右移了1位,再做或操作,使得n的二進(jìn)制表示中與最高位的1緊鄰的右邊一位也為1,如000011xxxxxx。
第二次右移
n |= n >>> 2;
注意,這個(gè)n已經(jīng)經(jīng)過(guò)了n |= n >>> 1; 操作。假設(shè)此時(shí)n為000011xxxxxx ,則n無(wú)符號(hào)右移兩位,會(huì)將最高位兩個(gè)連續(xù)的1右移兩位,然后再與原來(lái)的n做或操作,這樣n的二進(jìn)制表示的高位中會(huì)有4個(gè)連續(xù)的1。如00001111xxxxxx 。
第三次右移
n |= n >>> 4;
這次把已經(jīng)有的高位中的連續(xù)的4個(gè)1,右移4位,再做或操作,這樣n的二進(jìn)制表示的高位中會(huì)有8個(gè)連續(xù)的1。如00001111 1111xxxxxx 。
以此類推
注意,容量最大也就是32bit的正數(shù),因此最后n |= n >>> 16; ,最多也就32個(gè)1,但是這時(shí)已經(jīng)大于了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY 。
但是,請(qǐng)注意,在構(gòu)造方法中,并沒(méi)有對(duì)table這個(gè)成員變量進(jìn)行初始化,table的初始化被推遲到了put方法中,在put方法中會(huì)對(duì)threshold重新計(jì)算。
擾動(dòng)函數(shù)
HashMap 中的擾動(dòng)函數(shù)是一個(gè)通過(guò)對(duì) key 值類型自帶的哈希函數(shù)生成的散列值進(jìn)行位移計(jì)算來(lái)擾亂散列值,以達(dá)到降低哈希碰撞的概率的方法。源碼中對(duì)應(yīng)的是 hash(),但具體是如何進(jìn)行移位和降低碰撞概率的??
// jdk 8 static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }我們分析一下hash(),key.hash() 調(diào)用的是key類型自帶的哈希函數(shù),返回的是 int 類型的散列值。
如果沒(méi)有擾動(dòng)函數(shù)的情況下,我們拿著散列值作為下標(biāo)找到 hashmap 中對(duì)應(yīng)的桶位存下即可(不發(fā)送哈希沖突的情況下),但 int 類型是 32 位,很少有Hashmap的數(shù)組有40億這么大,所以, key 類型自帶的哈希函數(shù)返回的散列值不能拿來(lái)直接用。如果我們?nèi)〉蛶孜坏?hash 值來(lái)做數(shù)組映射行不行,但是如果低位相同,高位不同的 hash 值就碰撞了,如:
// Hash 碰撞示例: 00000000 00000000 00000000 00000101 & 1111 = 0101 // H1 00000000 11111111 00000000 00000101 & 1111 = 0101 // H2為了解決這個(gè)問(wèn)題,HashMap 想了個(gè)辦法,用擾動(dòng)函數(shù)降低碰撞的概率。將 hash 值右移16位(hash值的高16位)與 原 hash 值做異或運(yùn)算(^),從而得到一個(gè)新的散列值。如:
00000000 00000000 00000000 00000101 // H1 00000000 00000000 00000000 00000000 // H1 >>> 16 00000000 00000000 00000000 00000101 // hash = H1 ^ (H1 >>> 16) = 500000000 11111111 00000000 00000101 // H2 00000000 00000000 00000000 11111111 // H2 >>> 16 00000000 00000000 00000000 11111010 // hash = H2 ^ (H2 >>> 16) = 250H1,H2 兩個(gè) hash 值經(jīng)過(guò)擾動(dòng)后,很明顯不會(huì)發(fā)生碰撞。
總結(jié)
總的來(lái)說(shuō),不管是規(guī)定 Hashmap 的 n 為 2次冪,還是擾動(dòng)函數(shù),都是為了一個(gè)目標(biāo),降低哈希沖突的概率,從而使 HashMap 性能得到優(yōu)化。而規(guī)定 n 為 2次冪,是在新建 Hashmap對(duì)象初始化時(shí),規(guī)定其容量大小的角度來(lái)優(yōu)化。而擾動(dòng)函數(shù)是插入 key 值時(shí)改變 key 的散列值來(lái)達(dá)到優(yōu)化效果。
總結(jié)
以上是生活随笔為你收集整理的为什么HashMap默认初始容量为2次幂?不是2次幂会怎样?讲讲 HashMap 扰动函数?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 全球与中国单模连续光纤激光器市场现状及未
- 下一篇: jQuery笔记——工具函数——jQue