談?wù)凥ashmap的容量為什么是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函數(shù)由hashmap自己實現(xiàn) int i = indexFor(hash, table.length);//獲取將要存放的數(shù)組下標(biāo) /* * for中的代碼用于:當(dāng)hash值相同且key相同的情況下,使用新值覆蓋舊值(其實就是修改功能) */ for (Entry<K, V> e = table[i]; e != null; e = e.next) {//注意:for循環(huán)在第一次執(zhí)行時就會先判斷條件 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 } /** * "按位與"來獲取數(shù)組下標(biāo) */ static int indexFor(int h, int length) { return h & (length - 1); }
hashmap始終將自己的桶保持在2的n次方,這是為什么?indexFor這個方法解釋了這個問題
大家都知道計算機(jī)里面位運(yùn)算是基本運(yùn)算,位運(yùn)算的效率是遠(yuǎn)遠(yuǎn)高于取余%運(yùn)算的
舉個例子:
2^n轉(zhuǎn)換成二進(jìn)制就是1+n個0,減1之后就是0+n個1,如16 -> 10000,15 -> 01111
那么根據(jù)&位運(yùn)算的規(guī)則,都為1(真)時,才為1,那0≤運(yùn)算后的結(jié)果≤15,假設(shè)h <= 15,那么運(yùn)算后的結(jié)果就是h本身,h >15,運(yùn)算后的結(jié)果就是最后四位二進(jìn)制做&運(yùn)算后的值,最終,就是%運(yùn)算后的余數(shù)。
當(dāng)容量一定是2^n時,h & (length - 1) == h % length
補(bǔ)充知識:HashMap容量和負(fù)載因子
HashMap底層數(shù)據(jù)結(jié)構(gòu)是數(shù)組+鏈表,JDK1.8中還引入了紅黑樹,當(dāng)鏈表長度超過8個時,會將鏈表轉(zhuǎn)成紅黑樹,以提升其查找性能。那么,給出一個<key, value>節(jié)點(diǎn),HashMap是如何確定這個節(jié)點(diǎn)應(yīng)該放在具體哪個位置呢?(以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沒有被初始化,則先進(jìn)行初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 節(jié)點(diǎn)所在index = (n - 1) & hash,該位置沒有數(shù)據(jù),則直接將新節(jié)點(diǎn)放在數(shù)組的index位置上 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { // index上已經(jīng)有節(jié)點(diǎn)了 Node<K,V> e; K k; // 如果新key與原來的key一樣,則e指向原節(jié)點(diǎn)p(后面會用新value替換e所指向的value) if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 如果該節(jié)點(diǎn)是樹節(jié)點(diǎn),則采用樹的插入算法,插入新節(jié)點(diǎn) else if (p instanceof HashMap.TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 該節(jié)點(diǎn)是鏈表節(jié)點(diǎn) for (int binCount = 0; ; ++binCount) { // 將新節(jié)點(diǎn)插入到index所在鏈表的末端 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 鏈表節(jié)點(diǎn)超過8個,則進(jìn)行鏈表轉(zhuǎn)樹處理 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 同樣的,如果key已經(jīng)存在的話,則不進(jìn)行插入操作,而是后面進(jìn)行value替換 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // e != null的情況,就是key已經(jīng)存在了,這里統(tǒng)一進(jìn)行了新值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; // 插入后數(shù)組size 大于閾值的話,需要進(jìn)行擴(kuò)容 if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
看源碼,節(jié)點(diǎn)落在數(shù)組中的index = (數(shù)組長度 - 1) & key的hashcode,如果該index上沒有數(shù)據(jù),則直接插到該index上,如果節(jié)點(diǎn)已經(jīng)有數(shù)據(jù)了,則把新節(jié)點(diǎn)插入該index對應(yīng)的鏈表中(如果鏈表節(jié)點(diǎn)大于8個,會進(jìn)行鏈表轉(zhuǎn)樹,之后的插入算法就變成了樹的插入算法)。
每次put之后,會檢測一下是否需要擴(kuò)容,size超過了 總?cè)萘?* 負(fù)載因子,則會擴(kuò)容。默認(rèn)情況下,16 * 0.75 = 12個。
1、為什么初始容量是16
當(dāng)容量為2的冪時,上述n -1 對應(yīng)的二進(jìn)制數(shù)全為1,這樣才能保證它和key的hashcode做&運(yùn)算后,能夠均勻分布,這樣才能減少hash碰撞的次數(shù)。至于默認(rèn)值為什么是16,而不是2 、4、8,或者32、64、1024等,我想應(yīng)該就是個折中處理,過小會導(dǎo)致放不下幾個元素,就要進(jìn)行擴(kuò)容了,而擴(kuò)容是一個很消耗性能的操作。取值過大的話,無疑會浪費(fèi)更多的內(nèi)存空間。因此在日常開發(fā)中,如果可以預(yù)估HashMap會存入節(jié)點(diǎn)的數(shù)量,則應(yīng)該在初始化時,指定其容量。
2、為什么負(fù)載因子是0.75
也是一個綜合考慮,如果設(shè)置過小,HashMap每put少量的數(shù)據(jù),都要進(jìn)行一次擴(kuò)容,而擴(kuò)容操作會消耗大量的性能。如果設(shè)置過大的話,如果設(shè)成1,容量還是16,假設(shè)現(xiàn)在數(shù)組上已經(jīng)占用的15個,再要put數(shù)據(jù)進(jìn)來,計算數(shù)組index時,發(fā)生hash碰撞的概率將達(dá)到15/16,這違背的HashMap減少hash碰撞的原則。
以上這篇談?wù)凥ashmap的容量為什么是2的冪次問題就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringCloud中的openFeign調(diào)用服務(wù)并傳參的過程
服務(wù)和服務(wù)之間通信,不僅僅是調(diào)用,往往在調(diào)用過程中還伴隨著參數(shù)傳遞,接下來重點(diǎn)來看看OpenFeign在調(diào)用服務(wù)時如何傳遞參數(shù),感興趣的朋友一起看看吧2023-11-11避免sql注入_動力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了避免sql注入,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-08-08詳解Java中ByteArray字節(jié)數(shù)組的輸入輸出流的用法
ByteArrayInputStream和ByteArrayOutputStream分別集成自InputStream和OutputStream這兩個輸入和輸出流,這里我們就來詳解Java中ByteArray字節(jié)數(shù)組的輸入輸出流的用法,需要的朋友可以參考下2016-06-06Java用數(shù)組實現(xiàn)循環(huán)隊列的示例
下面小編就為大家?guī)硪黄狫ava用數(shù)組實現(xiàn)循環(huán)隊列的示例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09java socket接收保證能讀完數(shù)據(jù)的實例
這篇文章主要介紹了java socket接收保證能讀完數(shù)據(jù)的實例,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10Spring?security?oauth2以redis作為tokenstore及jackson序列化失敗問題
這篇文章主要介紹了Spring?security?oauth2以redis作為tokenstore及jackson序列化失敗問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教<BR>2024-04-04