HashMap的默認容量為什么要設置16?


 

在HashMap中,indexFor方法其實主要是將hashcode換成鏈表數組中的下標。

static int indexFor(int h, int length) {
    return h & (length-1);
}

這里實際就是取模。

用位運算是因為它比取模運算效率要高很多,因為它是直接對內存數據操作,不需要轉成十進制,因此處理速度非常快。

但是需要length是2^n, 這樣才滿足:

X % 2^n = X & (2^n – 1)

所以,HashMap的容量一定要是2^n。

那么為什么要是16呢?而不是4,8 ,32呢?

這應該是經驗值,需要在效率和內存使用上做一個權衡。這個值不能太大,也不能太小。

太小了就可能會頻繁的發生擴容,影響效率;太大了又浪費空間,不划算。

所以,16作為一個經驗值就被采用了。

那么HashMap如何保證其容量一定可以是2^n呢?

HashMap在兩個可能改變其容量的地方都做了兼容處理:

  1. 指定容量初始化值時;

  2. 擴容時;

指定容量初始化值時

HashMap根據用戶傳入的初始化容量,利用無符號右移和按位或運算等方式計算出第一個大於該數的2的冪。

看一下JDK是如何找到比傳入的指定值大的第一個2的冪的:

int n = cap - 1;
//step1 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;//step2

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

step1具體 怎么理解呢?其實是對一個二進制數依次向右移位,然后與原值取或。其目的對於一個數字的二進制,從第一個不為0的位開始,把后面的所有位都設置成1。隨便拿一個二進制數,套一遍上面的公式就發現其目的了:

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

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

另外注意:

在JDK 1.7和JDK 1.8中,HashMap初始化這個容量的時機不同

JDK 1.8中,在調用HashMap的構造函數定義HashMap的時候,就會進行容量的設定。

而在JDK 1.7中,要等到第一次put操作時才進行這一操作。

總之,HashMap根據用戶傳入的初始化容量,利用無符號右移和按位或運算等方式計算出第一個大於該數的2的冪

擴容

除了初始化的時候會指定HashMap的容量,在進行擴容的時候,其容量也可能會改變。

HashMap有擴容機制,就是當達到擴容條件時會進行擴容。

HashMap的擴容條件就是當HashMap中的元素個數(size)超過臨界值(threshold)時就會自動擴容。

在HashMap中,threshold = loadFactor * capacity。

loadFactor是裝載因子,表示HashMap滿的程度,默認值為0.75f,設置成0.75有一個好處,那就是0.75正好是3/4,而capacity又是2的冪。

所以,兩個數的乘積都是整數。

下面是HashMap中的擴容方法(resize)中的一段:

if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    oldCap >= DEFAULT_INITIAL_CAPACITY)
    newThr = oldThr << 1; // double threshold
}

從上面代碼可以看出,擴容后的table大小變為原來的兩倍,這一步執行之后,就會進行擴容后table的調整,這部分非本文重點,省略。

所以,通過保證初始化容量均為2的冪,並且擴容時也是擴容到之前容量的2倍,所以,保證了HashMap的容量永遠都是2的冪。

總結

HashMap作為一種數據結構,元素在put的過程中需要進行hash運算,目的是計算出該元素存放在hashMap中的具體位置。

hash運算的過程其實就是對目標元素的Key進行hashcode,再對Map的容量進行取模,而JDK 的工程師為了提升取模的效率,使用位運算代替了取模運算,這就要求Map的容量一定得是2的冪。

而作為默認容量,太大和太小都不合適,所以16就作為一個比較合適的經驗值被采用了。

為了保證任何情況下Map的容量都是2的冪,HashMap在兩個地方都做了限制。

首先是,如果用戶制定了初始容量,那么HashMap會計算出比該數大的第一個2的冪作為初始容量。

另外,在擴容的時候,也是進行成倍的擴容,即4變成8,8變成16。

 

參考:https://mp.weixin.qq.com/s/ktre8-C-cP_2HZxVW5fomQ


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM