在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