僅靠一個(gè)HashMap的講解打動(dòng)了字節(jié)面試官,我的秘訣是

image.png

預(yù)備知識(shí)

位運(yùn)算知識(shí)

位運(yùn)算操作是由處理器支持的底層操作暇藏,底層硬件只支持01這樣的數(shù)字,因此位運(yùn)算運(yùn)行速度很快濒蒋。盡管現(xiàn)代計(jì)算機(jī)處理器擁有了更長(zhǎng)的指令流水線和更優(yōu)的架構(gòu)設(shè)計(jì)盐碱,使得加法和乘法運(yùn)算幾乎與位運(yùn)算一樣快,但是位運(yùn)算消耗更少的資源沪伙。常用的位運(yùn)算如下:

位與 &

(1&1=1 1&0=0 0&0=0)

位或 |

(1|1=1 1|0=1 0|0=0)

位非 ~

( ~1=0 ~0=1)

位異或 ^

(1^1=0 1^0=1 0^0=0)

有符號(hào)右移 >>

在執(zhí)行右移操作時(shí)瓮顽,若參與運(yùn)算的數(shù)字為正數(shù),則在高位補(bǔ)0围橡;若為負(fù)數(shù)暖混,則在高位補(bǔ)1。

無(wú)符號(hào)右移 >>>

無(wú)論參與運(yùn)算的數(shù)字為正數(shù)或?yàn)樨?fù)數(shù)翁授,在執(zhí)運(yùn)算時(shí)拣播,都會(huì)在高位補(bǔ)0善绎。

左移

對(duì)于左移是沒(méi)有正數(shù)跟負(fù)數(shù)這一說(shuō)的,因?yàn)樨?fù)數(shù)在CPU中是以補(bǔ)碼的形式存儲(chǔ)的诫尽,對(duì)于正數(shù)左移相當(dāng)于乘以2的N次冪禀酱。

敲重點(diǎn):上面的重重都是簡(jiǎn)單的只是為了引出下面的結(jié)論:

a % (Math.pow(2,n)) 等價(jià)于 a&( Math.pow(2,n)-1)

比如a%16最終的結(jié)果一定是0~15之間的數(shù)字牧嫉,而a&1111正好把a(bǔ)除16后的余數(shù)有效的現(xiàn)實(shí)出來(lái)了因?yàn)槿绻? 1111這樣的話最前面一位其實(shí)代表的16剂跟,也就是說(shuō)二進(jìn)制從倒數(shù)第五位開(kāi)始只要出現(xiàn)了1那絕對(duì)代表的是16的倍數(shù)。 結(jié)論:位運(yùn)算比除法運(yùn)算在運(yùn)行效率上更高酣藻,對(duì)一個(gè)數(shù)取余盡量用a&二進(jìn)制數(shù)這樣可以更好的提速曹洽。

ArrayList

我們知道ArrayList是一個(gè)數(shù)組隊(duì)列,相當(dāng)于動(dòng)態(tài)數(shù)組辽剧。與Java中的基本數(shù)組相比送淆,它的容量能動(dòng)態(tài)增長(zhǎng)。它具有以下幾個(gè)重點(diǎn)怕轿。

ArrayList 實(shí)際上是通過(guò)一個(gè)數(shù)組去保存數(shù)據(jù)的偷崩。當(dāng)我們構(gòu)造ArrayList時(shí);若使用默認(rèn)構(gòu)造函數(shù)撞羽,則ArrayList的默認(rèn)容量大小是10阐斜。

當(dāng)ArrayList容量不足以容納全部元素時(shí),ArrayList會(huì)重新設(shè)置容量:原來(lái)容量的1.5倍诀紊。

ArrayList的克隆函數(shù)谒出,即是將全部元素克隆到一個(gè)數(shù)組中。

克隆的底層是System.arraycopy(0,oldsrc,0,newsrc,length);

ArrayList實(shí)現(xiàn)java.io.Serializable的方式邻奠。當(dāng)寫(xiě)入到輸出流時(shí)笤喳,先寫(xiě)入“容量”,再依次寫(xiě)入“每一個(gè)元素”碌宴;當(dāng)讀出輸入流時(shí)杀狡,先讀取“容量”,再依次讀取“每一個(gè)元素”唧喉。

優(yōu)點(diǎn):

根據(jù)下標(biāo)遍歷元素效率較高捣卤。

根據(jù)下標(biāo)訪問(wèn)元素效率較高。

在數(shù)組的基礎(chǔ)上封裝了對(duì)元素操作的方法八孝。

這樣的動(dòng)態(tài)數(shù)組在內(nèi)地地址上是空間連續(xù)的董朝。

可以自動(dòng)擴(kuò)容。

缺點(diǎn):

插入和刪除的效率比較低干跛。

根據(jù)內(nèi)容查找元素的效率較低子姜。

擴(kuò)容規(guī)則:每次擴(kuò)容現(xiàn)有容量的50%。

LinkedList

image

雙向鏈表每一個(gè)節(jié)點(diǎn)包含三部分(data,prev,next)楼入,它不要求空間是連續(xù)的哥捕。類(lèi)似于節(jié)點(diǎn)跟節(jié)點(diǎn)之間通過(guò)前后兩條線串聯(lián)起來(lái)的牧抽。

ArrayList和LinkedList總結(jié):

ArrayList是實(shí)現(xiàn)了基于動(dòng)態(tài)數(shù)組的數(shù)據(jù)結(jié)構(gòu),LinkedList是基于鏈表結(jié)構(gòu)遥赚。

對(duì)于隨機(jī)訪問(wèn)的get和set方法扬舒,ArrayList要優(yōu)于LinkedList,因?yàn)長(zhǎng)inkedList要移動(dòng)指針凫佛。

對(duì)于新增和刪除操作add和remove讲坎,LinkedList比較占優(yōu)勢(shì),因?yàn)锳rrayList要移動(dòng)數(shù)據(jù)愧薛。

ArrayList使用在查詢(xún)比較多晨炕,但是插入和刪除比較少的情況,而LinkedList用在查詢(xún)比較少而插入刪除比較多的情況

RedBlackTree

首先你需要對(duì)二叉樹(shù)有個(gè)了解毫炉,知道這是什么樣子的一個(gè)數(shù)據(jù)組合方式瓮栗,然后知道二叉樹(shù)查找的時(shí)候缺點(diǎn),可能發(fā)生數(shù)據(jù)傾斜瞄勾。因此引入了平衡二叉樹(shù)费奸,平衡二叉樹(shù)的左右節(jié)點(diǎn)深度之差不會(huì)超過(guò)1,查找方便構(gòu)建麻煩丰榴,因此又出現(xiàn)了紅黑樹(shù)货邓。紅黑樹(shù)是一種平衡的二叉查找樹(shù)秆撮,是一種計(jì)算機(jī)科學(xué)中常用的數(shù)據(jù)結(jié)構(gòu)四濒,最典型的應(yīng)用是實(shí)現(xiàn)數(shù)據(jù)的關(guān)聯(lián),例如map等數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)职辨,紅黑樹(shù)重要特性是( 左節(jié)點(diǎn) < 根節(jié)點(diǎn) < 右節(jié)點(diǎn)) 紅黑樹(shù)有以下限制:

節(jié)點(diǎn)必須是紅色或者是黑色

根節(jié)點(diǎn)是黑色的

所有的葉子節(jié)點(diǎn)是黑色的盗蟆。

每個(gè)紅色節(jié)點(diǎn)的兩個(gè)子節(jié)點(diǎn)是黑色的,也就是不能存在父子兩個(gè)節(jié)點(diǎn)全是紅色

從任意每個(gè)節(jié)點(diǎn)到其每個(gè)葉子節(jié)點(diǎn)的所有簡(jiǎn)單路徑上黑色節(jié)點(diǎn)的數(shù)量是相同的舒裤。

如果您對(duì)紅黑樹(shù)還不太了解推薦看下博主以前寫(xiě)的RBT

HashTable

Hash表是一種特殊的數(shù)據(jù)結(jié)構(gòu)喳资,它同數(shù)組、鏈表以及二叉排序樹(shù)等相比較有很明顯的區(qū)別腾供,它能夠快速定位到想要查找的記錄仆邓,而不是與表中存在的記錄的關(guān)鍵字進(jìn)行比較來(lái)進(jìn)行查找。這個(gè)源于Hash表設(shè)計(jì)的特殊性伴鳖,它采用了==函數(shù)映射==的思想將記錄的存儲(chǔ)位置與記錄的關(guān)鍵字關(guān)聯(lián)起來(lái)节值,從而能夠很快速地進(jìn)行查找。評(píng)價(jià)函數(shù)的性能關(guān)鍵在于==裝填因子==榜聂,以及如何合理的解決哈希沖突搞疗,具體的可看博主以前寫(xiě)的徹底搞定哈希表

HashMap源碼剖析

概述

通常具備前面一些知識(shí)點(diǎn)的鋪墊就可以很好的開(kāi)展HashMap的講解了,既然ArrayList须肆,LinkedList匿乃,Red Black Tree各有優(yōu)缺點(diǎn)桩皿,我們能不能集百家之長(zhǎng)實(shí)現(xiàn)一個(gè)綜合產(chǎn)物呢 === >HashMap,本文所以講解都是基于JDK8幢炸。

HashMap的組成部分:數(shù)組 + 鏈表 + 紅黑樹(shù)泄隔。HashMap的主干是一個(gè)Node數(shù)組。Node是HashMap的基本組成單元宛徊,每一個(gè)Node包含一個(gè)key-value鍵值對(duì)梅尤。HashMap的時(shí)間復(fù)雜度幾乎可以接近O(1)(如果出現(xiàn)了 哈希沖突可能會(huì)波動(dòng)下),并且HashMap的空間利用率一般就是在40%左右岩调。HashMap的大致圖如下:

image

PS:其中幾個(gè)重要節(jié)點(diǎn)關(guān)系如下:

1.java.util.Map.Entry 這就是個(gè)interface定義了一些比較的接口函數(shù)巷燥。

image

2.java.util.HashMap.Node 就是我們HashMap中存儲(chǔ)的基本的KV。

image

3.java.util.LinkedHashMap.Enrty Enrty這個(gè)類(lèi)繼承自HashMap.Node這個(gè)類(lèi)号枕,Enrty是LIinkedHashMap的一個(gè)內(nèi)部類(lèi)缰揪,

image

4.java.util.HashMap.TreeNode TreeNode的構(gòu)造函數(shù)向上追溯繼承了LinkedHashMap.Entry,而后者又繼承了HashMap.Node葱淳。所以TreeNode既保有Node的屬性钝腺,同時(shí)由于添加了prev這個(gè)前驅(qū)指針使得==鏈表==變?yōu)榱?=雙向==。前三個(gè)節(jié)點(diǎn)跟第五個(gè)紅黑樹(shù)相關(guān)赞厕,第四個(gè)跟next跟雙向鏈表相關(guān)艳狐。

image
image

數(shù)據(jù)存儲(chǔ)的大致步驟有三步。

5.每個(gè)數(shù)據(jù)通過(guò)HashTable里的映射函數(shù)來(lái)決定將該數(shù)據(jù)放到數(shù)組的那個(gè)地方皿桑,數(shù)組初始化時(shí)候一定是2的次冪毫目,默認(rèn)16,初始化傳入的任何數(shù)字都會(huì)經(jīng)過(guò)tableSizeFor調(diào)整為2次冪诲侮。

6.如果同一個(gè)數(shù)組的地方被分配到太多數(shù)據(jù)就用鏈表法來(lái)解決哈希沖突镀虐。

7.如果同一個(gè)節(jié)點(diǎn)的鏈表數(shù)據(jù)節(jié)點(diǎn)個(gè)數(shù) > TREEIFY_THRESHOLD=8且數(shù)組長(zhǎng)度 >= MIN_TREEIFY_CAPACITY=64,則會(huì)將該鏈表進(jìn)化為RedBlackTree,如果RedBlackTree中節(jié)點(diǎn)個(gè)數(shù)小于UNTREEIFY_THRESHOLD=6會(huì)退化為鏈表沟绪。

特別提醒:讀HashMap源碼之前需要知道它大致特性如下:

1.HashMap的存取是沒(méi)有順序的

2.KV均允許為NULL

3.多線程情況下該類(lèi)安全刮便,可以考慮用HashTable。

4.JDk8底層是數(shù)組 + 鏈表 + 紅黑樹(shù)绽慈,JDK7底層是數(shù)組 + 鏈表恨旱。

5.初始容量和裝載因子是決定整個(gè)類(lèi)性能的關(guān)鍵點(diǎn),輕易不要?jiǎng)印?/p>

6.HashMap是懶漢式創(chuàng)建的,只有在你put數(shù)據(jù)時(shí)候才會(huì)build

7.單向鏈表轉(zhuǎn)換為紅黑樹(shù)的時(shí)候會(huì)先變化為雙向鏈表最終轉(zhuǎn)換為紅黑樹(shù)坝疼,雙向鏈表跟紅黑樹(shù)是共存的搜贤,切記。

8.對(duì)于傳入的兩個(gè)key裙士,會(huì)強(qiáng)制性的判別出個(gè)高低入客,判別高低主要是為了決定向左還是向右。

9.鏈表轉(zhuǎn)紅黑樹(shù)后會(huì)努力將紅黑樹(shù)的root節(jié)點(diǎn)和鏈表的頭節(jié)點(diǎn) 跟table[i]節(jié)點(diǎn)融合成一個(gè)。

10.在刪除的時(shí)候是先判斷刪除節(jié)點(diǎn)紅黑樹(shù)個(gè)數(shù)是否需要轉(zhuǎn)鏈表桌硫,不轉(zhuǎn)鏈表就跟RBT類(lèi)似夭咬,找個(gè)合適的節(jié)點(diǎn)來(lái)填充已刪除的節(jié)點(diǎn)。

11.紅黑樹(shù)的root節(jié)點(diǎn)不一定跟table[i]也就是鏈表的頭節(jié)點(diǎn)是同一個(gè)哦铆隘,三者同步是靠MoveRootToFront實(shí)現(xiàn)的哨苛。而HashIterator.remove()會(huì)在調(diào)用removeNode的時(shí)候movable=false慷荔。

image

在這里插入圖片描述

重要參數(shù)

靜態(tài)參數(shù)
  1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4

初始容量洲脂,默認(rèn)容量=16竹勉,箱子的個(gè)數(shù)不能太多或太少。如果太少肿嘲,很容易觸發(fā)擴(kuò)容融击,如果太多,遍歷哈希表會(huì)比較慢雳窟。

  1. static final int MAXIMUM_CAPACITY = 1 << 30

數(shù)組的最大容量尊浪,一般情況下只要內(nèi)存夠用,哈希表不會(huì)出現(xiàn)問(wèn)題

  1. static final float DEFAULT_LOAD_FACTOR = 0.75f

默認(rèn)的負(fù)載因子封救。因此初始情況下拇涤,當(dāng)存儲(chǔ)的所有節(jié)點(diǎn)數(shù) > (16 * 0.75 = 12 )時(shí),就會(huì)觸發(fā)擴(kuò)容誉结。 默認(rèn)負(fù)載因子(0.75)在時(shí)間和空間成本上提供了很好的折衷鹅士。較高的值會(huì)降低空間開(kāi)銷(xiāo),但提高查找成本(體現(xiàn)在大多數(shù)的HashMap類(lèi)的操作惩坑,包括get和put)掉盅。設(shè)置初始大小時(shí),應(yīng)該考慮預(yù)計(jì)的entry數(shù)在map及其負(fù)載系數(shù)旭贬,并且盡量減少rehash操作的次數(shù)怔接。如果初始容量大于最大條目數(shù)除以負(fù)載因子,rehash操作將不會(huì)發(fā)生稀轨。

從上面的表中可以看到當(dāng)桶中元素到達(dá)8個(gè)的時(shí)候,概率已經(jīng)變得非常小岸军,也就是說(shuō)用0.75作為加載因子奋刽,每個(gè)碰撞位置的鏈表長(zhǎng)度超過(guò)8?jìng)€(gè)是幾乎不可能的。 4. static final int TREEIFY_THRESHOLD = 8

這個(gè)值表示當(dāng)某個(gè)箱子(數(shù)組的某個(gè)item)中艰赞,鏈表長(zhǎng)度 >= 8 時(shí)佣谐,有可能會(huì)轉(zhuǎn)化成樹(shù)。設(shè)置為8方妖,是系統(tǒng)根據(jù)泊松分布的數(shù)據(jù)分布圖來(lái)設(shè)定的狭魂。

  1. static final int UNTREEIFY_THRESHOLD = 6

在哈希表擴(kuò)容時(shí),如果發(fā)現(xiàn)鏈表長(zhǎng)度 <= 6,則會(huì)由樹(shù)重新退化為鏈表雌澄。 設(shè)置為6猜測(cè)是因?yàn)闀r(shí)間和空間的權(quán)衡

當(dāng)鏈表長(zhǎng)度為6時(shí) 查詢(xún)的平均長(zhǎng)度為 n/2=3斋泄,紅黑樹(shù) log(6) = 2.6 為8時(shí) :鏈表 8/2=4, 紅黑樹(shù) log(8)=3

  1. static final int MIN_TREEIFY_CAPACITY = 64

鏈表轉(zhuǎn)變成樹(shù)之前镐牺,還會(huì)有一次判斷炫掐,只有數(shù)組長(zhǎng)度大于 64 才會(huì)發(fā)生轉(zhuǎn)換。這是為了避免在哈希表建立初期睬涧,多個(gè)鍵值對(duì)恰好被放入了同一個(gè)鏈表中而導(dǎo)致不必要的轉(zhuǎn)化募胃。

動(dòng)態(tài)參數(shù)
  1. transient Node<K,V>[] table

HashMap的鏈表數(shù)組。無(wú)論我們初始化時(shí)候是否傳參畦浓,它在自擴(kuò)容時(shí)總是2的次冪痹束。

  1. transient Set<Map.Entry<K,V>> entrySet

HashMap實(shí)例中的Entry的Set集合

  1. transient int size

HashMap表中存儲(chǔ)的實(shí)例KV個(gè)數(shù)。

  1. transient int modCount

凡是我們做的增刪改都會(huì)引發(fā)modCount值的變化讶请,跟版本控制功能類(lèi)似参袱,可以理解成version,在特定的操作下需要對(duì)version進(jìn)行檢查秽梅,適用于Fai-Fast機(jī)制抹蚀。

在java的集合類(lèi)中存在一種Fail-Fast的錯(cuò)誤檢測(cè)機(jī)制,當(dāng)多個(gè)線程對(duì)同一集合的內(nèi)容進(jìn)行操作時(shí)企垦,可能就會(huì)產(chǎn)生此類(lèi)異常环壤。 比如當(dāng)A通過(guò)iterator去遍歷某集合的過(guò)程中,其他線程修改了此集合钞诡,此時(shí)會(huì)拋出ConcurrentModificationException異常郑现。 此類(lèi)機(jī)制就是通過(guò)modCount實(shí)現(xiàn)的,在迭代器初始化時(shí)荧降,會(huì)賦值expectedModCount接箫,在迭代過(guò)程中判斷modCount和expectedModCount是否一致。

  1. int threshold

擴(kuò)容閾值 threshold = capacity * loadFactor

  1. final float loadFactor

可自定義的負(fù)載因子朵诫,不過(guò)一般都是用系統(tǒng)自帶的0.75辛友。

四種構(gòu)造方法

1.默認(rèn)構(gòu)造方法

image

2.傳入初始容量大小

image

3.傳入初始容量大小及負(fù)載因子

image

==tableSizeFor==:作用是返回大于輸入?yún)?shù)且最小的2的整數(shù)次冪的數(shù)。比如10剪返,則返回16废累。

image

詳解如下:

先來(lái)分析有關(guān)n位操作部分:先來(lái)假設(shè)n的二進(jìn)制為01xxx...xxx。接著 對(duì)n右移1位:001xx...xxx脱盲,再位或:011xx...xxx 對(duì)n右移2為:00011...xxx邑滨,再位或:01111...xxx 此時(shí)前面已經(jīng)有四個(gè)1了,再右移4位且位或可得8個(gè)1 同理钱反,有8個(gè)1掖看,右移8位肯定會(huì)讓后八位也為1匣距。

綜上可得,該算法讓最高位的1后面的位全變?yōu)?哎壳。最后再讓結(jié)果n+1毅待,即得到了2的整數(shù)次冪的值了。 現(xiàn)在回來(lái)看看第一條語(yǔ)句:

int n = cap - 1;

讓cap-1再賦值給n的目的是另找到的目標(biāo)值大于或等于原值耳峦。例如二進(jìn)制1000恩静,十進(jìn)制數(shù)值為8。如果不對(duì)它減1而直接操作蹲坷,將得到答案10000驶乾,即16。顯然不是結(jié)果循签。減1后二進(jìn)制為111级乐,再進(jìn)行操作則會(huì)得到原來(lái)的數(shù)值1000,這種二進(jìn)制方法的效率非常高县匠。

  1. 構(gòu)造函數(shù)傳入一個(gè)map 使用默認(rèn)的負(fù)載因子风科,然后根據(jù)當(dāng)前map的大小來(lái)反推需要的threshold,同時(shí)還可能會(huì)涉到resize乞旦,然后住個(gè)put到 容器中贼穆。
image

Hash值

無(wú)論我們put數(shù)據(jù)還是get數(shù)據(jù)都要先獲得該數(shù)據(jù)在這個(gè)哈希表中對(duì)應(yīng)的位置。比如put數(shù)據(jù)兰粉,它的流程分為2步故痊。

1.先獲得key對(duì)應(yīng)的hash值。 2. 將該數(shù)據(jù)的hash值A(chǔ)玖姑,跟將A右無(wú)符號(hào)移動(dòng)16位后再^得到最終值愕秫。這個(gè)操作叫擾動(dòng),原因是怕低幾位出現(xiàn)想同的概率太大焰络,盡可能的將數(shù)據(jù)實(shí)現(xiàn)均勻分布戴甩。

image

同時(shí)JDK8跟JDK7的擾動(dòng)目的一樣,不過(guò)復(fù)雜程度不一樣闪彼。

image

1. get

相對(duì)來(lái)說(shuō)很簡(jiǎn)單,為方便理解先說(shuō)下代碼大致流程思路甜孤。

獲得key的hash然后根據(jù)hash和key按照插入時(shí)候的思路去查找get。

如果數(shù)組位置為NULL則直接返回 NULL备蚓。

如果數(shù)組位置不為NULL則先直接看數(shù)組位置是否符合课蔬。

如果數(shù)組位置有類(lèi)型說(shuō)紅黑樹(shù)類(lèi)型,則按照紅黑樹(shù)類(lèi)型查找返回郊尝。

如果數(shù)組有next,則按照遍歷鏈表的方式查找返回战惊。

image

1.1 getNode

宏觀查找函數(shù)細(xì)節(jié):

image

1.3 getTreeNode

紅黑樹(shù)查找節(jié)點(diǎn)細(xì)節(jié):

先獲得根節(jié)點(diǎn)流昏,左節(jié)點(diǎn)扎即,右節(jié)點(diǎn)。

根據(jù) 左節(jié)點(diǎn) < 根節(jié)點(diǎn) < 右節(jié)點(diǎn) 對(duì)對(duì)數(shù)據(jù)進(jìn)行逐步范圍的縮小查找况凉。

如果實(shí)現(xiàn)了Comparable方法則直接對(duì)比谚鄙。

否則如果根節(jié)點(diǎn)不符合則遞歸性的調(diào)用find查找函數(shù)。

image

1.4 ComparableClassFor:

查詢(xún)?cè)搆ey是否實(shí)現(xiàn)了Comparable接口刁绒。

image

1.5 compareComparables:

既然實(shí)現(xiàn)了Comparable接口就用該實(shí)現(xiàn)進(jìn)行對(duì)比判斷如何何去何從闷营。

image

2. put流程

跟隨源碼梳理下put操作的大致流程。

image

數(shù)據(jù)插入的時(shí)候大致流程如下:

  1. 對(duì)數(shù)據(jù)進(jìn)行Hash值計(jì)算知市。
  2. 將數(shù)據(jù)插入前先查看下當(dāng)前table的狀態(tài)傻盟,如果table是空需要調(diào)用resize來(lái)進(jìn)行初始化。
  3. 通過(guò)位運(yùn)算獲得key的目標(biāo)位置嫂丙。并判斷當(dāng)前位置情況娘赴。
  4. 如果當(dāng)前位置為空則直接進(jìn)行放置,如果跟當(dāng)前key一直則進(jìn)行覆蓋跟啤。
  5. 如果當(dāng)前有數(shù)據(jù)則看當(dāng)前數(shù)據(jù)類(lèi)型是否是紅黑樹(shù)诽表,是的話需要調(diào)用putTreeVal。
  6. 否則就認(rèn)為是個(gè)鏈表隅肥,然后循環(huán)的查找進(jìn)行尾部==插入==竿奏。同時(shí)還要考慮當(dāng)前鏈表轉(zhuǎn)紅黑樹(shù)。

在JDK8中尋找待插入點(diǎn) e是通過(guò)==尾插法==(類(lèi)似與排隊(duì)在最后面)腥放,而在JDK7中是==前插法==(類(lèi)似與加塞在最前面泛啸,之所以這樣做是HashMap發(fā)明者認(rèn)為后插入節(jié)被訪問(wèn)概率更大),對(duì)應(yīng)代碼如下捉片。

image
  1. 對(duì)找到的舊節(jié)點(diǎn)e進(jìn)行判斷

oldValue對(duì)應(yīng)的舊值如果為NULL平痰,那么無(wú)論onlyIfAbsent是否決定替換。都將被替換伍纫。

oldValue對(duì)應(yīng)的舊值如果不為NULL宗雇,那么如果onlyIfAbsent是false就替換。

onlyIfAbsent:只有在缺席的情況下才替換莹规,不缺席不替換赔蒲。跟redis Setnx 同樣的功能。

  1. 數(shù)據(jù)最終添加完畢后要對(duì)對(duì)修改后的變量modCount加1良漱,同時(shí)看最新的總的節(jié)點(diǎn)數(shù)是否需要擴(kuò)容了舞虱,如果是就擴(kuò)容。

2 put

image

2.1 putTreeVal

  1. 先找到根節(jié)點(diǎn)母市,然后判斷是從左邊找還是右邊找key矾兜。
  2. 找到了則直接返回找到的節(jié)點(diǎn)。
  3. 沒(méi)找到則新建節(jié)點(diǎn)將該新建節(jié)點(diǎn)放到適當(dāng)?shù)奈恢没季茫瑫r(shí)考慮紅黑樹(shù)跟雙向鏈表的節(jié)點(diǎn)插入情況椅寺。
image

2.2 treeifyBin

主要功能是根據(jù)參數(shù)的閾值范圍絕對(duì)是否將鏈表轉(zhuǎn)化為紅黑樹(shù)浑槽,然后首先將單項(xiàng)鏈表轉(zhuǎn)化為雙向鏈表,再調(diào)用treeify以頭節(jié)點(diǎn)為根節(jié)點(diǎn)構(gòu)建紅黑樹(shù)返帕。

image

2.3 treeify

雙向鏈表跟紅黑樹(shù)創(chuàng)建桐玻,主要步驟分3步。

  1. 從該雙向鏈表中找第一個(gè)節(jié)點(diǎn)為root節(jié)點(diǎn)荆萤。
  2. 每一個(gè)雙向鏈表節(jié)點(diǎn)都在root節(jié)點(diǎn)為根都二叉樹(shù)中找位置然后將該數(shù)據(jù)插入到紅黑樹(shù)中镊靴,同時(shí)要注意balance
  3. 最終要注意將根節(jié)點(diǎn)跟當(dāng)前table[i]對(duì)應(yīng)好链韭。
image

2.4 moveRootToFront

確保將root節(jié)點(diǎn)挪動(dòng)到table[first]上偏竟,如果紅黑樹(shù)構(gòu)建成功而沒(méi)成功執(zhí)行這個(gè)任務(wù)會(huì)導(dǎo)致tablle[first]對(duì)應(yīng)的節(jié)點(diǎn)不是紅黑樹(shù)的root節(jié)點(diǎn)。正常執(zhí)行的時(shí)候主要步驟分2步梧油。

  1. 找到跟節(jié)點(diǎn)然后將root節(jié)點(diǎn)放到跟節(jié)點(diǎn)苫耸,至此關(guān)于紅黑樹(shù)到操作搞定。
  2. 原來(lái)鏈表頭是first節(jié)點(diǎn)儡陨,現(xiàn)在將可能是中間節(jié)點(diǎn)的root節(jié)點(diǎn)挪到first節(jié)點(diǎn)前面褪子。 其中 checkInvariants函數(shù)的作用:校驗(yàn)TreeNode對(duì)象是否滿(mǎn)足紅黑樹(shù)和雙鏈表的特性。因?yàn)椴l(fā)情況下會(huì)發(fā)生很多異常骗村。
image

3 resize

獲得老table數(shù)據(jù)嫌褪,如果老table已經(jīng)足夠大則不再擴(kuò)容,只調(diào)節(jié)閾值胚股。

老table擴(kuò)容后的范圍也符合要求直接將容器大小跟閾值都擴(kuò)容 笼痛。

如果是帶參數(shù)構(gòu)造函數(shù)則需要將閾值復(fù)制給容器容量。

否則認(rèn)為該容器初始化時(shí)未傳參琅拌,需初始化缨伊。

如果老table有數(shù)據(jù),新他變了大小設(shè)置好了但是閾值沒(méi)設(shè)置成功进宝。此時(shí)要設(shè)置新閾值刻坊。

創(chuàng)建新容器。

老table成功擴(kuò)容為新table党晋,涉及到數(shù)據(jù)的轉(zhuǎn)移谭胚。

數(shù)據(jù)不為空是單獨(dú)的節(jié)點(diǎn)則直接重新hash分配新位置。

數(shù)據(jù)不為空后面是一個(gè)鏈表未玻,則要把鏈表數(shù)據(jù)進(jìn)行區(qū)分看那些分到老地方那些分到新地方灾而。

如果該節(jié)點(diǎn)類(lèi)型是個(gè)紅黑樹(shù)則調(diào)用split.

image

鏈表形式的重新劃分解釋如下: 注意:不是(e.hash & (oldCap-1))而是(e.hash & oldCap), 后一個(gè)得到的是 元素的在數(shù)組中的位置是否需要移動(dòng),示例如下

示例1: e.hash= 10 0000 1010 oldCap=16 0001 0000 & =0 0000 0000 比較高位的第一位 0 結(jié)論:元素位置在擴(kuò)容后數(shù)組中的位置沒(méi)有發(fā)生改變 示例2: e.hash= 17 0001 0001 oldCap=16 0001 0000 & =1 0001 0000 比較高位的第一位 1 結(jié)論:元素位置在擴(kuò)容后數(shù)組中的位置發(fā)生了改變,新的下標(biāo)位置是原下標(biāo)位置+原數(shù)組長(zhǎng)度

image

3.1 split

擴(kuò)容后如何處理原來(lái)一個(gè)table[i]上的紅黑樹(shù)扳剿,代碼的整體思路跟處理鏈表的時(shí)候差不多旁趟,只要理解節(jié)點(diǎn)關(guān)系保存紅黑樹(shù)的時(shí)候也保存了雙向鏈表就OK了。

image

4. find

函數(shù)功能就是以指定的一個(gè)節(jié)點(diǎn)為根節(jié)點(diǎn)庇绽,根據(jù)指定的key跟value進(jìn)行查找轻庆。

通過(guò)hash值判斷 左邊找還是右邊找癣猾。

如果找到的很簡(jiǎn)單直接返回敛劝。

可能出現(xiàn)hash值相等可是key不一樣余爆,繼續(xù)查找分為三種情況。

左節(jié)點(diǎn)為空則找右節(jié)點(diǎn)

右節(jié)點(diǎn)為空則找左節(jié)點(diǎn)

左右節(jié)點(diǎn)都不會(huì)空夸盟,嘗試通Comparable對(duì)數(shù)據(jù)看向左還是向右蛾方。

無(wú)法通過(guò)comparable比較或者比較之后還是相等。

直接從右節(jié)點(diǎn)遞歸查找下上陕。

否則就從左邊查找桩砰。

image

4.1 tieBreakOrder

對(duì)兩個(gè)對(duì)象進(jìn)行比較,一定能比出個(gè)高低释簿。

a 跟 b 都是字符串則直接在if判斷里比拼完畢

a 跟 b 都是對(duì)象則直接查看對(duì)象在JVM中的hash地址亚隅,然后比較。

image

4. remove

函數(shù)入口而已:

image

4.1 removeNode

removeNode無(wú)非就是查看table[i]是否存在庶溶,然后是否在首節(jié)點(diǎn)上煮纵,是否在紅黑樹(shù)上,是否在鏈表上偏螺。這幾種情況行疏,找到了則直接刪除,同時(shí)注意平衡性套像。

image

4.2 removeTreeNode

該函數(shù)的 目的就是移除調(diào)用此方法的節(jié)點(diǎn)酿联,也就是該方法中的this節(jié)點(diǎn)。移除包括鏈表的處理和紅黑樹(shù)的處理夺巩≌耆茫可以看以前寫(xiě)過(guò)的RBT,刪除的時(shí)候思路大致是一樣的柳譬,這里大致分為3步驟喳张。

紅黑樹(shù)也是雙向鏈表,以鏈表的角度來(lái)刪除節(jié)點(diǎn)征绎,然后判斷是否需要退化為鏈表蹲姐。

根據(jù)當(dāng)前的p節(jié)點(diǎn)嘗試從pr找最的或者從pl找最的目標(biāo)節(jié)點(diǎn)s,將兩點(diǎn)兌換人柿。

找到要replacement來(lái)跟p進(jìn)行替換柴墩。

實(shí)施替換。

替換后為保持紅黑樹(shù)特性可能需要進(jìn)行balance凫岖。

image

4.3 untreeify

紅黑樹(shù)退化成鏈表

image

4.4 balanceDeletion

關(guān)于這個(gè)問(wèn)題可以直接看博主以前寫(xiě)的紅黑叔添加跟刪除RBT

JDK7死環(huán)問(wèn)題

JDK7對(duì)舊table數(shù)據(jù)重定位到新table的函數(shù)transfer如下江咳,其中重點(diǎn)關(guān)注部分以標(biāo)出。

image

1.頭插法正常情況下:

image

2.并發(fā)情況下 線程1只執(zhí)行了Entry<K,V> next = e.next就被掛起了哥放,而線程2正常執(zhí)行完畢歼指,結(jié)果圖如下:

image

線程1接著下面繼續(xù)執(zhí)行: 通過(guò)逐步分析跟繪圖可以知道 會(huì)有環(huán)產(chǎn)生爹土。

image

HashIterator 的 remove 方法

7vs8

7中找Hash用了4次,8中只用了1次踩身。

7 = 數(shù)組 + 鏈表胀茵,8 = 數(shù)組 + 鏈表 + 紅黑樹(shù)

7中是頭插法,多線程容易造成環(huán)挟阻,8中是尾插法琼娘。

7的擴(kuò)容是全部數(shù)據(jù)重新定位,8中是位置不變+ 移動(dòng)舊size大小來(lái)實(shí)現(xiàn)更好些附鸽。

7是先判斷是否要擴(kuò)容再插入脱拼,8中是先插入再看是否要擴(kuò)容。

HashMap不管78都是現(xiàn)場(chǎng)不安全的坷备,多線程情況下記得用ConcurrentHashmap熄浓。ConcurrentHashmap下篇文章說(shuō)。

常見(jiàn)問(wèn)題

隨機(jī)搜羅了一些常見(jiàn)HashMap問(wèn)題省撑,如果把上述代碼都看懂了應(yīng)付這些應(yīng)該沒(méi)問(wèn)題赌蔑。

HashMap原理,內(nèi)部數(shù)據(jù)結(jié)構(gòu)丁侄。

HashMap中的put,get,remove大致過(guò)程惯雳。

HashMap中 hash函數(shù)實(shí)現(xiàn)。

HashMap如何擴(kuò)容鸿摇。

HashMap幾個(gè)重要參數(shù)為什么這樣設(shè)定石景。

HashMap為什么線程不安全,如何替換拙吉。

HashMap在JDK7跟JDK8中的區(qū)別潮孽。

HashMap中鏈表跟紅黑樹(shù)切換思路。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筷黔,一起剝皮案震驚了整個(gè)濱河市往史,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌佛舱,老刑警劉巖椎例,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異请祖,居然都是意外死亡订歪,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)肆捕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)刷晋,“玉大人,你說(shuō)我怎么就攤上這事⊙凼” “怎么了喻奥?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)捏悬。 經(jīng)常有香客問(wèn)我撞蚕,道長(zhǎng),這世上最難降的妖魔是什么邮破? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任诈豌,我火速辦了婚禮,結(jié)果婚禮上抒和,老公的妹妹穿的比我還像新娘。我一直安慰自己彤蔽,他們只是感情好摧莽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著顿痪,像睡著了一般镊辕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蚁袭,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天征懈,我揣著相機(jī)與錄音,去河邊找鬼揩悄。 笑死卖哎,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的删性。 我是一名探鬼主播亏娜,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蹬挺!你這毒婦竟也來(lái)了维贺?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤巴帮,失蹤者是張志新(化名)和其女友劉穎溯泣,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體榕茧,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡垃沦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了雪猪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片栏尚。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出译仗,到底是詐尸還是另有隱情抬虽,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布纵菌,位于F島的核電站阐污,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏咱圆。R本人自食惡果不足惜笛辟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望序苏。 院中可真熱鬧手幢,春花似錦、人聲如沸忱详。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)匈睁。三九已至监透,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間航唆,已是汗流浹背胀蛮。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留糯钙,地道東北人粪狼。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像超营,于是被迫代替她去往敵國(guó)和親鸳玩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容