談談Hashmap的容量為什麼是2的冪次問題
做為面試常考的問題之一,每次都答的模模糊糊,有必要了解一下,首先來看一下hashmap的put方法的原始碼
public V put(K key,V value) { if (key == null) return putForNullKey(value); //將空key的Entry加入到table[0]中 int hash = hash(key.hashCode()); //計算key.hashcode()的hash值,hash函式由hashmap自己實現 int i = indexFor(hash,table.length);//獲取將要存放的陣列下標 /* * for中的程式碼用於:當hash值相同且key相同的情況下,使用新值覆蓋舊值(其實就是修改功能) */ for (Entry<K,V> e = table[i]; e != null; e = e.next) {//注意:for迴圈在第一次執行時就會先判斷條件 Object k; //hash值相同且key相同的情況下,使用新值覆蓋舊值 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; //e.recordAccess(this); return oldValue;//返回舊值 } } modCount++; addEntry(hash,key,value,i);//增加一個新的Entry到table[i] return null;//如果沒有與傳入的key相等的Entry,就返回null } /** * "按位與"來獲取陣列下標 */ static int indexFor(int h,int length) { return h & (length - 1); }
hashmap始終將自己的桶保持在2的n次方,這是為什麼?indexFor這個方法解釋了這個問題
大家都知道計算機裡面位運算是基本運算,位運算的效率是遠遠高於取餘%運算的
舉個例子:
2^n轉換成二進位制就是1+n個0,減1之後就是0+n個1,如16 -> 10000,15 -> 01111
那麼根據&位運算的規則,都為1(真)時,才為1,那0≤運算後的結果≤15,假設h <= 15,那麼運算後的結果就是h本身,h >15,運算後的結果就是最後四位二進位制做&運算後的值,最終,就是%運算後的餘數。
當容量一定是2^n時,h & (length - 1) == h % length
補充知識:HashMap容量和負載因子
HashMap底層資料結構是陣列+連結串列,JDK1.8中還引入了紅黑樹,當連結串列長度超過8個時,會將連結串列轉成紅黑樹,以提升其查詢效能。那麼,給出一個<key,value>節點,HashMap是如何確定這個節點應該放在具體哪個位置呢?(以JDK1.8為例)
final V putVal(int hash,K key,V value,boolean onlyIfAbsent,boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n,i; // HashMap沒有被初始化,則先進行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 節點所在index = (n - 1) & hash,該位置沒有資料,則直接將新節點放在陣列的index位置上 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash,null); else { // index上已經有節點了 Node<K,V> e; K k; // 如果新key與原來的key一樣,則e指向原節點p(後面會用新value替換e所指向的value) if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 如果該節點是樹節點,則採用樹的插入演算法,插入新節點 else if (p instanceof HashMap.TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this,tab,hash,value); else { // 該節點是連結串列節點 for (int binCount = 0; ; ++binCount) { // 將新節點插入到index所在連結串列的末端 if ((e = p.next) == null) { p.next = newNode(hash,null); // 連結串列節點超過8個,則進行連結串列轉樹處理 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab,hash); break; } // 同樣的,如果key已經存在的話,則不進行插入操作,而是後面進行value替換 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // e != null的情況,就是key已經存在了,這裡統一進行了新值value,替換舊值e.value的操作 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 插入後陣列size 大於閾值的話,需要進行擴容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
看原始碼,節點落在陣列中的index = (陣列長度 - 1) & key的hashcode,如果該index上沒有資料,則直接插到該index上,如果節點已經有資料了,則把新節點插入該index對應的連結串列中(如果連結串列節點大於8個,會進行連結串列轉樹,之後的插入演算法就變成了樹的插入演算法)。
每次put之後,會檢測一下是否需要擴容,size超過了 總容量 * 負載因子,則會擴容。預設情況下,16 * 0.75 = 12個。
1、為什麼初始容量是16
當容量為2的冪時,上述n -1 對應的二進位制數全為1,這樣才能保證它和key的hashcode做&運算後,能夠均勻分佈,這樣才能減少hash碰撞的次數。至於預設值為什麼是16,而不是2 、4、8,或者32、64、1024等,我想應該就是個折中處理,過小會導致放不下幾個元素,就要進行擴容了,而擴容是一個很消耗效能的操作。取值過大的話,無疑會浪費更多的記憶體空間。因此在日常開發中,如果可以預估HashMap會存入節點的數量,則應該在初始化時,指定其容量。
2、為什麼負載因子是0.75
也是一個綜合考慮,如果設定過小,HashMap每put少量的資料,都要進行一次擴容,而擴容操作會消耗大量的效能。如果設定過大的話,如果設成1,容量還是16,假設現在陣列上已經佔用的15個,再要put資料進來,計算陣列index時,發生hash碰撞的概率將達到15/16,這違背的HashMap減少hash碰撞的原則。
以上這篇談談Hashmap的容量為什麼是2的冪次問題就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。