(給ImportNew加星標,提高Java技能)
1. HashMap的底層數據結構是什麼?
在JDK1.7中和JDK1.8中有所區別:
在JDK1.7中,由」數組+鍊表「組成,數組是HashMap的主體,鍊表則是主要為了解決哈希衝突而存在的。
在JDK1.8中,有「數組+鍊表+紅黑樹」組成。當鍊表過長,則會嚴重影響HashMap的性能,紅黑樹搜索時間複雜度是O(logn),而鍊表是O(n)。因此,JDK1.8對數據結構做了進一步的優化,引入了紅黑樹,鍊表和紅黑樹在達到一定條件會進行轉換:
當鍊表超過8且數組長度(數據總量)超過64才會轉為紅黑樹
將鍊表轉換成紅黑樹前會判斷,如果當前數組的長度小於64,那麼會選擇先進行數組擴容,而不是轉換為紅黑樹,以減少搜索時間。
2. 說一下HashMap的特點
hashmap存取是無序的
鍵和值位置都可以是null,但是鍵位置只能是一個null
鍵位置是唯一的,底層的數據結構是控制鍵的
jdk1.8前數據結構是:鍊表+數組jdk1.8之後是:數組+鍊表+紅黑樹
閾值(邊界值)>8並且數組長度大於64,才將鍊表轉換成紅黑樹,變成紅黑樹的目的是提高搜索速度,高效查詢
3. 解決hash衝突的辦法有哪些?HashMap用的哪種?
解決Hash衝突方法有:開放定址法、再哈希法、鏈地址法(HashMap中常見的拉鏈法)、簡歷公共溢出區。HashMap中採用的是鏈地址法。
開放定址法也稱為再散列法,基本思想就是,如果p=H(key)出現衝突時,則以p為基礎,再次hash,p1=H(p),如果p1再次出現衝突,則以p1為基礎,以此類推,直到找到一個不衝突的哈希地址pi。因此開放定址法所需要的hash表的長度要大於等於所需要存放的元素,而且因為存在再次hash,所以只能在刪除的節點上做標記,而不能真正刪除節點
再哈希法(雙重散列,多重散列),提供多個不同的hash函數,R1=H1(key1)發生衝突時,再計算R2=H2(key1),直到沒有衝突為止。這樣做雖然不易產生堆集,但增加了計算的時間。
鏈地址法(拉鏈法),將哈希值相同的元素構成一個同義詞的單鍊表,並將單鍊表的頭指針存放在哈希表的第i個單元中,查找、插入和刪除主要在同義詞鍊表中進行,鍊表法適用於經常進行插入和刪除的情況。
建立公共溢出區,將哈希表分為公共表和溢出表,當溢出發生時,將所有溢出數據統一放到溢出區
注意開放定址法和再哈希法的區別是
開放定址法只能使用同一種hash函數進行再次hash,再哈希法可以調用多種不同的hash函數進行再次hash
4. 為什麼要在數組長度大於64之後,鍊表才會進化為紅黑樹
在數組比較小時如果出現紅黑樹結構,反而會降低效率,而紅黑樹需要進行左旋右旋,變色,這些操作來保持平衡,同時數組長度小於64時,搜索時間相對要快些,總之是為了加快搜索速度,提高性能
JDK1.8以前HashMap的實現是數組+鍊表,即使哈希函數取得再好,也很難達到元素百分百均勻分布。當HashMap中有大量的元素都存放在同一個桶中時,這個桶下有一條長長的鍊表,此時HashMap就相當於單鍊表,假如單鍊表有n個元素,遍歷的時間複雜度就從O(1)退化成O(n),完全失去了它的優勢,為了解決此種情況,JDK1.8中引入了紅黑樹(查找的時間複雜度為O(logn))來優化這種問題
5. 為什麼加載因子設置為0.75,初始化臨界值是12?
HashMap中的threshold是HashMap所能容納鍵值對的最大值。計算公式為length*LoadFactory。也就是說,在數組定義好長度之後,負載因子越大,所能容納的鍵值對個數也越大
loadFactory越趨近於1,那麼數組中存放的數據(entry也就越來越多),數據也就越密集,也就會有更多的鍊表長度處於更長的數值,我們的查詢效率就會越低,當我們添加數據,產生hash衝突的概率也會更高
默認的loadFactory是0.75,loadFactory越小,越趨近於0,數組中個存放的數據(entry)也就越少,表現得更加稀疏
0.75是對空間和時間效率的一種平衡選擇
如果負載因子小一些比如是0.4,那麼初始長度16*0.4=6,數組占滿6個空間就進行擴容,很多空間可能元素很少甚至沒有元素,會造成大量的空間被浪費
如果負載因子大一些比如是0.9,這樣會導致擴容之前查找元素的效率非常低
loadfactory設置為0.75是經過多重計算檢驗得到的可靠值,可以最大程度的減少rehash的次數,避免過多的性能消耗
6. 哈希表底層採用何種算法計算hash值?還有哪些算法可以計算出hash值?
hashCode方法是Object中的方法,所有的類都可以對其進行使用,首先底層通過調用hashCode方法生成初始hash值h1,然後將h1無符號右移16位得到h2,之後將h1與h2進行按位異或(^)運算得到最終hash值h3,之後將h3與(length-1)進行按位與(&)運算得到hash表索引
其他可以計算出hash值的算法有
平方取中法
取餘數
偽隨機數法
7. 當兩個對象的hashCode相等時會怎樣
hashCode相等產生hash碰撞,hashCode相等會調用equals方法比較內容是否相等,內容如果相等則會進行覆蓋,內容如果不等則會連接到鍊表後方,鍊表長度超過8且數組長度超過64,會轉變成紅黑樹節點
8. 何時發生哈希碰撞和什麼是哈希碰撞,如何解決哈希碰撞?
只要兩個元素的key計算的hash碼值相同就會發生hash碰撞,jdk8之前使用鍊表解決哈希碰撞,jdk8之後使用鍊表+紅黑樹解決哈希碰撞
9. HashMap的put方法流程
以jdk8為例,簡要流程如下:
首先根據key的值計算hash值,找到該元素在數組中存儲的下標
如果數組是空的,則調用resize進行初始化;
如果沒有哈希衝突直接放在對應的數組下標里
如果衝突了,且key已經存在,就覆蓋掉value
如果衝突後是鍊表結構,就判斷該鍊表是否大於8,如果大於8並且數組容量小於64,就進行擴容;如果鍊表節點數量大於8並且數組的容量大於64,則將這個結構轉換成紅黑樹;否則,鍊表插入鍵值對,若key存在,就覆蓋掉value
如果衝突後,發現該節點是紅黑樹,就將這個節點掛在樹上
10. HashMap的擴容方式
HashMap在容量超過負載因子所定義的容量之後,就會擴容。java里的數組是無法自己擴容的,將HashMap的大小擴大為原來數組的兩倍
我們來看jdk1.8擴容的源碼
擴容之後原位置的節點只有兩種調整
保持原位置不動(新bit位為0時)
散列原索引+擴容大小的位置去(新bit位為1時)
擴容之後元素的散列設置的非常巧妙,節省了計算hash值的時間,我們來看一 下具體的實現
當數組長度從16到32,其實只是多了一個bit位的運算,我們只需要在意那個多出來的bit為是0還是1,是0的話索引不變,是1的話索引變為當前索引值+擴容的長度,比如5變成5+16=21
這樣的擴容方式不僅節省了重新計算hash的時間,而且保證了當前桶中的元素總數一定小於等於原來桶中的元素數量,避免了更嚴重的hash衝突,均勻的把之前衝突的節點分散到新的桶中去
11. 一般用什麼作為HashMap的key?
一般用Integer、String這種不可變類當HashMap當key
因為String是不可變的,當創建字符串時,它的hashcode被緩存下來,不需要再次計算,相對於其他對象更快
因為獲取對象的時候要用到equals()和hashCode()方法,那麼鍵對象正確的重寫這兩個方法是非常重要的,這些類很規範的重寫了hashCode()以及equals()方法
12. 為什麼Map桶中節點個數超過8才轉為紅黑樹?
8作為閾值作為HashMap的成員變量,在源碼的注釋中並沒有說明閾值為什麼是8
在HashMap中有這樣一段注釋說明,我們繼續看
翻譯
樹節點占用空間是普通Node的兩倍,如果鍊表節點不夠多卻轉換成紅黑樹,無疑會耗費大量的空間資源,並且在隨機hash算法下的所有bin節點分布頻率遵從泊松分布,鍊表長度達到8的概率只有0.00000006,幾乎是不可能事件,所以8的計算是經過重重科學考量的
從平均查找長度來看,紅黑樹的平均查找長度是logn,如果長度為8,則logn=3,而鍊表的平均查找長度為n/4,長度為8時,n/2=4,所以閾值8能大大提高搜索速度
當長度為6時紅黑樹退化為鍊表是因為logn=log6約等於2.6,而n/2=6/2=3,兩者相差不大,而紅黑樹節點占用更多的內存空間,所以此時轉換最為友好
13. HashMap為什麼線程不安全?
多線程下擴容死循環。JDK1.7中的HashMap使用頭插法插入元素,在多線程的環境下,擴容的時候有可能導致環形鍊表的出現,形成死循環。因此JDK1.8使用尾插法插入元素,在擴容時會保持鍊表元素原本的順序,不會出現環形鍊表的問題
多線程的put可能導致元素的丟失。多線程同時執行put操作,如果計算出來的索引位置是相同的,那會造成前一個key被後一個key覆蓋,從而導致元素的丟失。此問題在JDK1.7和JDK1.8中都存在
put和get並發時,可能導致get為null。線程1執行put時,因為元素個數超出threshold而導致rehash,線程2此時執行get,有可能導致這個問題,此問題在JDK1.7和JDK1.8中都存在
14. 計算hash值時為什麼要讓低16bit和高16bit進行異或處理
我們計算索引需要將hashCode值與length-1進行按位與運算,如果數組長度很小,比如16,這樣的值和hashCode做異或實際上只有hashCode值的後4位在進行運算,hash值是一個隨機值,而如果產生的hashCode值高位變化很大,而低位變化很小,那麼有很大概率造成哈希衝突,所以我們為了使元素更好的散列,將hash值的高位也利用起來\
舉個例子
如果我們不對hashCode進行按位異或,直接將hash和length-1進行按位與運算就有可能出現以下的情況
如果下一次生成的hashCode值高位起伏很大,而低位幾乎沒有變化時,高位無法參與運算
可以看到,兩次計算出的hash相等,產生了hash衝突
所以無符號右移16位的目的是使高混亂度地區與地混亂度地區做一個中和,提高低位的隨機性,減少哈希衝突
- EOF -
Java面試奪命21連問!(附答案)
JDK動態代理為什麼必須要基於接口?
一個 HashMap 跟面試官扯了半個小時
看完本文有收穫?請轉發分享給更多人
關注「ImportNew」,提升Java技能
點讚和在看就是最大的支持❤️