如何選擇HashMap的默認容量,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
創新互聯是一家專業提供壽光企業網站建設,專注與網站設計、成都網站設計、H5場景定制、小程序制作等業務。10年已為壽光眾多企業、政府機構等服務。創新互聯專業網站設計公司優惠進行中。集合是Java開發日常開發中經常會使用到的,而作為一種典型的K-V結構的數據結構,HashMap對于Java開發者一定不陌生。在日常開發中,我們經常會像如下方式以下創建一個HashMap:Map但是,大家有沒有想過,上面的代碼中,我們并沒有給HashMap指定容量,那么,這時候一個新創建的HashMap的默認容量是多少呢?為什么呢?本文就來分析下這個問題。map = new HashMap ();
Map輸出結果:map = new HashMap ();
map.put("hollis", "hollischuang");
Class> mapType = map.getClass();
Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : " + capacity.invoke(map));
Field size = mapType.getDeclaredField("size");
size.setAccessible(true);
System.out.println("size : " + size.get(map));
capacity : 16、size : 1上面我們定義了一個新的HashMap,并向其中put了一個元素,然后通過反射的方式打印capacity和size,其容量是16,已經存放的元素個數是1。通過前面的例子,我們發現了,當我們創建一個HashMap的時候,如果沒有指定其容量,那么會得到一個默認容量為16的Map,那么,這個容量是怎么來的呢?又為什么是這個數字呢?
hash :該方法主要是將Object轉換成一個整型。indexFor :該方法主要是將hash生成的整型轉換成鏈表數組中的下標。為了聚焦本文的重點,我們只來看一下indexFor方法。我們先來看下Java 7(Java8中雖然沒有這樣一個單獨的方法,但是查詢下標的算法也是和Java 7一樣的)中該實現細節:
static int indexFor(int h, int length) {indexFor方法其實主要是將hashcode換成鏈表數組中的下標。其中的兩個參數h表示元素的hashcode值,length表示HashMap的容量。那么return h & (length-1) 是什么意思呢?其實,他就是取模。Java之所有使用位運算(&)來代替取模運算(%),最主要的考慮就是效率。
return h & (length-1);
}
位運算(&)效率要比代替取模運算(%)高很多,主要原因是位運算直接對內存數據進行操作,不需要轉成十進制,因此處理速度非常快。那么,為什么可以使用位運算(&)來實現取模運算(%)呢?這實現的原理如下:
X % 2^n = X & (2^n – 1)假設n為3,則2^3 = 8,表示成2進制就是1000。2^3 -1 = 7 ,即0111。此時X & (2^3 – 1) 就相當于取X的2進制的最后三位數。從2進制角度來看,X / 8相當于 X >> 3,即把X右移3位,此時得到了X / 8的商,而被移掉的部分(后三位),則是X % 8,也就是余數。上面的解釋不知道你有沒有看懂,沒看懂的話其實也沒關系,你只需要記住這個技巧就可以了。或者你可以找幾個例子試一下。
6 % 8 = 6 ,6 & 7 = 6
10 & 8 = 2 ,10 & 7 = 2
運算過程如下如:
所以,return h & (length-1);只要保證length的長度是2^n 的話,就可以實現取模運算了。
所以,因為位運算直接對內存數據進行操作,不需要轉成十進制,所以位運算要比取模運算的效率更高,所以HashMap在計算元素要存放在數組中的index的時候,使用位運算代替了取模運算。之所以可以做等價代替,前提是要求HashMap的容量一定要是2^n 。那么,既然是2^n ,為啥一定要是16呢?為什么不能是4、8或者32呢?關于這個默認容量的選擇,JDK并沒有給出官方解釋,筆者也沒有在網上找到關于這個任何有價值的資料。(如果哪位有相關的權威資料或者想法,可以留言交流)根據作者的推斷,這應該就是個經驗值(Experience Value),既然一定要設置一個默認的2^n 作為初始值,那么就需要在效率和內存使用上做一個權衡。這個值既不能太小,也不能太大。太小了就有可能頻繁發生擴容,影響效率。太大了又浪費空間,不劃算。所以,16就作為一個經驗值被采用了。在JDK 8中,關于默認容量的定義為:static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 ,其故意把16寫成1<<4,就是提醒開發者,這個地方要是2的冪。值得玩味的是:注釋中的 aka 16 也是1.8中新增的,那么,接下來我們再來談談,HashMap是如何保證其容量一定可以是2^n 的呢?如果用戶自己設置了的話又會怎么樣呢?關于這部分,HashMap在兩個可能改變其容量的地方都做了兼容處理,分別是指定容量初始化時以及擴容時。
在JDK 1.7和JDK 1.8中,HashMap初始化這個容量的時機不同。JDK 1.8中,在調用HashMap的構造函數定義HashMap的時候,就會進行容量的設定。而在JDK 1.7中,要等到第一次put操作時才進行這一操作。看一下JDK是如何找到比傳入的指定值大的第一個2的冪的:
int n = cap - 1;上面的算法目的挺簡單,就是:根據用戶傳入的容量值(代碼中的cap),通過計算,得到第一個比他大的2的冪并返回。
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;
Step 1,5->7Step 2,7->8Step 1,9->15Step 2,15->16Step 1,19->31Step 2,31->32對應到以上代碼中,Step1:
n |= n >>> 1;對應到以上代碼中,Step2:
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;Step 2 比較簡單,就是做一下極限值的判斷,然后把Step 1得到的數值+1。Step 1 怎么理解呢?其實是對一個二進制數依次向右移位,然后與原值取或。其目的對于一個數字的二進制,從第一個不為0的位開始,把后面的所有位都設置成1。隨便拿一個二進制數,套一遍上面的公式就發現其目的了:
1100 1100 1100 >>>1 = 0110 0110 0110通過幾次無符號右移和按位或運算,我們把1100 1100 1100轉換成了1111 1111 1111 ,再把1111 1111 1111加1,就得到了1 0000 0000 0000,這就是大于1100 1100 1100的第一個2的冪。好了,我們現在解釋清楚了Step 1和Step 2的代碼。就是可以把一個數轉化成第一個比他自身大的2的冪。但是還有一種特殊情況套用以上公式不行,這些數字就是2的冪自身。如果數字4套用公式的話。得到的會是 8,不過其實這個問題也被解決了,具體驗證辦法及JDK的解決方案見全網把Map中的hash()分析的最透徹的文章,別無二家。這里就不再展開了。總之,HashMap根據用戶傳入的初始化容量,利用無符號右移和按位或運算等方式計算出第一個大于該數的2的冪。
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
if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&從上面代碼可以看出,擴容后的table大小變為原來的兩倍,這一步執行之后,就會進行擴容后table的調整,這部分非本文重點,省略。可見,當HashMap中的元素個數(size)超過臨界值(threshold)時就會自動擴容,擴容成原容量的2倍,即從16擴容到32、64、128 …所以,通過保證初始化容量均為2的冪,并且擴容時也是擴容到之前容量的2倍,所以,保證了HashMap的容量永遠都是2的冪。
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}