一文帶你解讀所有HashMap的面試題
關(guān)于 HashMap 阿粉相信大家再面試的時候,是非常容易被問到的,為什么呢?因為至少是在 JDK8 出來之后,非常容易被問到關(guān)于 HashMap 的知識點,而如果對于沒有研究過他的源代碼的同學(xué)來說,這個可能只是說出一部分來,比如線程安全,鏈表+紅黑樹,以及他的擴(kuò)容等等,今天阿粉就來把 HashMap 上面大部分會被在面試中問到的內(nèi)容,做個總結(jié)。
HashMap
說到 HashMap 想必大家從腦海中直接復(fù)現(xiàn)出了一大堆的面試題,
- HashMap 的數(shù)據(jù)結(jié)構(gòu)
- JDK7 和 JDK8 HashMap哪里不一樣
- HashMap是否安全
- HashMap 的擴(kuò)容機(jī)制
說到這里,我們就來挨著分析一下這個 HashMap 的這寫面試題。
HashMap 的數(shù)據(jù)結(jié)構(gòu)
這個 HashMap 的數(shù)據(jù)結(jié)構(gòu),面試官這個問題,屬于那種可大可小的,往大了說,那就是需要你把所有的關(guān)于 HashMap 中的內(nèi)容都詳細(xì)的解釋明白,但是如果要是往小了說,那就是介紹一下內(nèi)部結(jié)構(gòu),就可以了。
阿粉今天來分析一下這個數(shù)據(jù)結(jié)構(gòu)了。
HashMap 里面有幾個比較重要的參數(shù):
//默認(rèn)初始容量——必須是2的冪 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//當(dāng)沒有構(gòu)造函數(shù)中指定使用的負(fù)載系數(shù) static final float DEFAULT_LOAD_FACTOR = 0.75f;
//擴(kuò)容的閾值,等于 CAPACITY * LOAD_FACTOR static final int TREEIFY_THRESHOLD = 8;
//降容的閾值 static?final?int?UNTREEIFY_THRESHOLD?=?6;
//擴(kuò)容的另外一個參數(shù) static?final?int?MIN_TREEIFY_CAPACITY?=?64;
參數(shù)我們都看到了上述的這些內(nèi)容了,如果用大白話,怎么去形容這些參數(shù)呢?其實這就涉及到這個后面的 JDK8 中的 HashMap 不一樣的結(jié)構(gòu)了,
我們也知道 JDK8 中的 HashMap ,如果在橫向上是數(shù)組的話,那么他的縱向的每一個元素上面,都是一個單項的鏈表,而這個鏈表,會根據(jù)長度,來進(jìn)行不通的演化,而這個演化就是擴(kuò)容成為樹結(jié)構(gòu)和降容成為鏈表結(jié)構(gòu)的關(guān)鍵,而這些關(guān)鍵,都是通過這些參數(shù)來進(jìn)行的定義。
CAPACITY
就相當(dāng)于是 HashMap 中的默認(rèn)初始容量。
LOAD_FACTOR
負(fù)載因子
TREEIFY_THRESHOLD
樹化的閾值,也就是說table的node中的鏈表長度超過這個閾值的時候,該鏈表會變成樹
UNTREEIFY_THRESHOLD
樹降級成為鏈表的閾值(也就是說table的node中的樹長度低于這個閾值的時候,樹會變成鏈表)
MIN_TREEIFY_CAPACITY
樹化的另一個參數(shù),就是當(dāng)hashmap中的node的個數(shù)大于這個值的時候,hashmap中的有些鏈表才會變成樹。
transient Node<K,V>[] table
Hash 表
有些小伙伴在面試的時候,就會說,當(dāng) HashMap中的某個 node 鏈表長度大于 8 的時候,HashMap 中的這個鏈表就會變成樹,實際上不是的,這個還和 MIN_TREEIFY_CAPACITY
有關(guān)系,也就是說整個 HashMap 的 node 數(shù)量大于64,node 的鏈表長度大于 8 才會變成樹。
JDK7 和 JDK8 HashMap哪里不一樣
JDK7我們大家也都知道,如果按照橫向是數(shù)組,那么他的縱向每個元素上面,都是一個單向的鏈表,而橫向上,每一個實體,就相當(dāng)于是一個 Entry 的實例。
而這每一個 Entry 中都包含了四個屬性,
- key
- value
- hash值
- 用于單項列表的next
就像下圖這個樣子
JDK7
所以 JDK7 的 HashMap 的數(shù)據(jù)結(jié)構(gòu)就是 數(shù)組+鏈表 的形式構(gòu)成
而 JDK8 就不一樣了,因為他們的內(nèi)部很巧妙的給增加了紅黑樹,如下圖
JDK8
所以 JDK8 的 HashMap 的數(shù)據(jù)結(jié)構(gòu)就是 數(shù)組+鏈表+紅黑樹
的形式構(gòu)成了。
HashMap是否安全
一說這個,肯定都是非?;A(chǔ)的面試題,都知道 HashMap 是屬于那種線程不安全的類,為什么不安全,他不安全到底會提現(xiàn)在哪個地方,難道面試的時候,你就只會說他的內(nèi)部沒有被 synchronize 關(guān)鍵字控制么?
所以,說起 HashMap 的不安全,那么就得從 put 和 get 方法說起了。
這個直接先看內(nèi)部實現(xiàn),我們先來看 put 方法,然后去分析這個 put 方法,
public?V?put(K?key,?V?value)?{ ????????return?putVal(hash(key),?key,?value,?false,?true); ????} final?V?putVal(int?hash,?K?key,?V?value,?boolean?onlyIfAbsent, ???????????????????boolean?evict)?{ ????????Node<K,V>[]?tab;?Node<K,V>?p;?int?n,?i; ????????//在這里先進(jìn)行?Hash表的初始化 ????????if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0) ????????????n?=?(tab?=?resize()).length; ????????//通過?Hash?值計算在?Hash?表中的位置,并將這個位置的元素賦值給P?如果等于空的話創(chuàng)建一個新的?node ????????if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null) ????????????tab[i]?=?newNode(hash,?key,?value,?null); ????????else?{ ????????????Node<K,V>?e;?K?k; ????????????//Hash表的當(dāng)前的?index?已經(jīng)存在了元素,向這個元素后追加鏈表 ????????????if?(p.hash?==?hash?&& ????????????????((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k)))) ????????????????e?=?p; ????????????else?if?(p?instanceof?TreeNode) ????????????????e?=?((TreeNode<K,V>)p).putTreeVal(this,?tab,?hash,?key,?value); ????????????else?{ ????????????????for?(int?binCount?=?0;?;?++binCount)?{ ????????????????????//新建接點,并且追加到列表 ????????????????????if?((e?=?p.next)?==?null)?{ ????????????????????????p.next?=?newNode(hash,?key,?value,?null); ????????????????????????if?(binCount?>=?TREEIFY_THRESHOLD?-?1)?//?-1?for?1st ????????????????????????????treeifyBin(tab,?hash); ????????????????????????break; ????????????????????} ????????????????????if?(e.hash?==?hash?&& ????????????????????????((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k)))) ????????????????????????break; ????????????????????p?=?e; ????????????????} ????????????} ????????????if?(e?!=?null)?{?//?existing?mapping?for?key ????????????????V?oldValue?=?e.value; ????????????????if?(!onlyIfAbsent?||?oldValue?==?null) ????????????????????e.value?=?value; ????????????????afterNodeAccess(e); ????????????????return?oldValue; ????????????} ????????} ????????++modCount; ????????if?(++size?>?threshold) ????????????resize(); ????????afterNodeInsertion(evict); ????????return?null; ????}
看到源碼之后,我們猜想一下都會有哪些地方會出問題呢?比如,這時候如果有兩個線程同時去執(zhí)行 put
一個線程 A 執(zhí)行put("1","A");
一個線程 B 執(zhí)行put("2","B");
如果這個時候線程 A 和 B 都執(zhí)行了 if ((p = tab[i = (n - 1) & hash]) == null)
但是,如果這個時候線程 A 先執(zhí)行了 tab[i] = newNode(hash, key, value, null);
這時候,內(nèi)部是沒啥問題的,已經(jīng)放進(jìn)去了,
這時候如果線程 B 去執(zhí)行 tab[i] = newNode(hash, key, value, null);
就會導(dǎo)致 A 線程中的 key 為 1 的元素 A 丟失。直接被線程 B 進(jìn)行了覆蓋,這也是為什么會有一些人說, JDK7 中是對擴(kuò)容時會造成環(huán)形鏈或數(shù)據(jù)丟失,而在 JDK8 中是會會發(fā)生數(shù)據(jù)覆蓋的情況。
就會出現(xiàn) null 的問題,這個問題,不論是 JDK7 還是 JDK8 全都有這個問題,如果面試的時候,能夠從這個地方分析一下的,至少這個線程不安全,你確實是自己去研究了一下,所以這就可以完美的解釋了,HashMap 的線程不安全的問題了。
HashMap 的擴(kuò)容機(jī)制
我們在上面也都列舉了一下 HashMap 的一些關(guān)鍵參數(shù),接下來,就來分析他的擴(kuò)容是怎么實現(xiàn)的 ,
????public?HashMap(int?initialCapacity,?float?loadFactor)?{ ????????if?(initialCapacity?<?0) ????????????throw?new?IllegalArgumentException("Illegal?initial?capacity:?"?+ ???????????????????????????????????????????????initialCapacity); ????????if?(initialCapacity?>?MAXIMUM_CAPACITY) ????????????initialCapacity?=?MAXIMUM_CAPACITY; ????????if?(loadFactor?<=?0?||?Float.isNaN(loadFactor)) ????????????throw?new?IllegalArgumentException("Illegal?load?factor:?"?+ ???????????????????????????????????????????????loadFactor); ????????this.loadFactor?=?loadFactor; ????????this.threshold?=?tableSizeFor(initialCapacity); ????}
這段代碼,寫的看起來非常的舒服,指定了初始容量和加載因子,下一次需要擴(kuò)容的容量 threshold
值由 tableSizeFor
方法得出
static?final?int?tableSizeFor(int?cap)?{ ????????int?n?=?cap?-?1; ????????//??>>>:無符號右移。無論是正數(shù)還是負(fù)數(shù),高位通通補0。 ????????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; ????}
而 tableSizeFor
這個方法是用于計算出大于等于 cap 值的最大的2的冪值,而后續(xù) HashMap 需要擴(kuò)容時,每次 table 數(shù)組長度都擴(kuò)展為原來的兩倍,所以,table 數(shù)組長度總是為2的冪值。
為什么用位移運算,不直接使用.pow的方法, 這個東西,很明顯, 位運算這種方式,效率可比.pow的效率要高很多,接下來就是正兒八經(jīng)的擴(kuò)容方法了。
????final?Node<K,V>[]?resize()?{ ????????Node<K,V>[]?oldTab?=?table; ????????int?oldCap?=?(oldTab?==?null)???0?:?oldTab.length;?//舊容量 ????????int?oldThr?=?threshold;//?舊的需要擴(kuò)容的閾值 ????????int?newCap,?newThr?=?0; ????????if?(oldCap?>?0)?{//如果不是第一次擴(kuò)容 ????????????if?(oldCap?>=?MAXIMUM_CAPACITY)?{ ????????????????threshold?=?Integer.MAX_VALUE;//?如果容量大于最大值,將閾值設(shè)為最大值,這樣不會發(fā)生下次擴(kuò)容 ????????????????return?oldTab; ????????????} ????????????//?擴(kuò)容容量為上一次容量的兩倍 ????????????else?if?((newCap?=?oldCap?<<?1)?<?MAXIMUM_CAPACITY?&& ?????????????????????oldCap?>=?DEFAULT_INITIAL_CAPACITY) ????????????????newThr?=?oldThr?<<?1;?//?下次擴(kuò)容閾值等于本次擴(kuò)容閾值*2,因為擴(kuò)容會擴(kuò)為原來容量的兩倍,所以依然滿足?newThr?=?newCap?*?loadFactor ????????} ????????else?if?(oldThr?>?0)?//?第一次擴(kuò)容,并且用戶指定了初始容量 ????????????newCap?=?oldThr;?//?擴(kuò)展的容量為閾值 ????????else?{???????????????//?第一次擴(kuò)容,并且初始容量和加載因子使用的默認(rèn)值 ????????????newCap?=?DEFAULT_INITIAL_CAPACITY; ????????????newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY); ????????} ????????if?(newThr?==?0)?{??//?如果用戶指定了初始容量時,并且是第一次擴(kuò)容 ????????????float?ft?=?(float)newCap?*?loadFactor; ????????????//?下次擴(kuò)容閾值為?newCap?*?loadFactor ????????????newThr?=?(newCap?<?MAXIMUM_CAPACITY?&&?ft?<?(float)MAXIMUM_CAPACITY?? ??????????????????????(int)ft?:?Integer.MAX_VALUE); ????????} ????????threshold?=?newThr; ????????@SuppressWarnings({"rawtypes","unchecked"}) ????????Node<K,V>[]?newTab?=?(Node<K,V>[])new?Node[newCap];//?新的數(shù)組 ????????table?=?newTab; ????????if?(oldTab?!=?null)?{ ????????????for?(int?j?=?0;?j?<?oldCap;?++j)?{?//將舊數(shù)組數(shù)據(jù)移動到新數(shù)組 ????????????????Node<K,V>?e; ????????????????if?((e?=?oldTab[j])?!=?null)?{ ????????????????????oldTab[j]?=?null; ????????????????????if?(e.next?==?null)?//?如果還不是鏈表或紅黑樹,把數(shù)據(jù)直接移動到新數(shù)組中對應(yīng)位置 ????????????????????????newTab[e.hash?&?(newCap?-?1)]?=?e; ????????????????????else?if?(e?instanceof?TreeNode)?//紅黑樹時的移動數(shù)據(jù) ????????????????????????((TreeNode<K,V>)e).split(this,?newTab,?j,?oldCap); ????????????????????else?{?//?鏈表時移動數(shù)據(jù) ????????????????????????//?原來的key的hash值對應(yīng)的數(shù)組位置可能會發(fā)生變化 ?????????????????????//?因為在做與操作時,現(xiàn)有的數(shù)組長度多了兩倍,也就是多了一位的與計算 ?????????????????????//?所以,鏈表或紅黑樹中的元素可能在原來位置,或者在原來位置?+?原來數(shù)組長度?的位置 ????????????????????????Node<K,V>?loHead?=?null,?loTail?=?null; ????????????????????????Node<K,V>?hiHead?=?null,?hiTail?=?null; ????????????????????????Node<K,V>?next; ????????????????????????do?{ ????????????????????????....省略不分 ????????????????????} ????????????????} ????????????} ????????} ????????return?newTab; ????}
總的來說,擴(kuò)容就是創(chuàng)建一個新的數(shù)組,數(shù)組長度為原來的兩倍,并將下一次需要擴(kuò)容的閾值設(shè)置為新數(shù)組乘以加載因子的大小。
然后將原來數(shù)組中的數(shù)據(jù)移動到新數(shù)組中。
如果數(shù)組中的元素不是鏈表和紅黑樹,那么直接移動到原來舊數(shù)組中下標(biāo)的位置。
否則如果是鏈表或紅黑樹,那么其中的數(shù)據(jù)可能會在原來的位置,或者在原來的位置+原來數(shù)組長度的位置,此時將原來的鏈表或紅黑樹分為兩個鏈表或紅黑樹,再把數(shù)據(jù)移動到相應(yīng)位置。
到此這篇關(guān)于一文帶你解讀所有HashMap的面試題的文章就介紹到這了,更多相關(guān)HashMap面試題內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java實現(xiàn)解析json復(fù)雜數(shù)據(jù)的方法詳解
這篇文章主要為大家詳細(xì)介紹了java如何實現(xiàn)解析json復(fù)雜數(shù)據(jù),文中的示例代碼講解詳細(xì),具有一定的借鑒價值,感興趣的小伙伴可以學(xué)習(xí)一下2024-01-01淺談Java中浮點型數(shù)據(jù)保留兩位小數(shù)的四種方法
今天在進(jìn)行開發(fā)的過程中遇到了一個小問題,是關(guān)于如何將double類型的數(shù)據(jù)保留兩位小數(shù)。具有一定的參考價值,本文就詳細(xì)的介紹一下2021-09-09Java tomcat環(huán)境變量及idea配置解析
這篇文章主要介紹了Java tomcat環(huán)境變量及idea配置解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-12-12Spring Boot 使用 logback、logstash、ELK 記錄日志文件的方法
這篇文章主要介紹了Spring Boot 使用 logback、logstash、ELK 記錄日志文件的思路詳解,文中給大家提到了logback 取代 log4j的理由,需要的朋友可以參考下2017-12-12Springboot如何設(shè)置靜態(tài)資源緩存一年
這篇文章主要介紹了Springboot如何設(shè)置靜態(tài)資源緩存一年,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-11-11部署springboot項目到云服務(wù)器的兩種方式(jar+war)
本文主要介紹了部署springboot項目到云服務(wù)器的兩種方式,主要介紹了jar和war兩種方式,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12Springboot項目對數(shù)據(jù)庫用戶名密碼實現(xiàn)加密過程解析
這篇文章主要介紹了Springboot項目對數(shù)據(jù)庫用戶名密碼實現(xiàn)加密過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-06-06