靠一個HashMap的講解打動了頭條面試官,個人祕訣是...

在這裏插入圖片描述
在這裏插入圖片描述

預備知識

位運算知識

位運算操做是由處理器支持的底層操做,底層硬件只支持01這樣的數字,所以位運算運行速度很快。儘管現代計算機處理器擁有了更長的指令流水線和更優的架構設計,使得加法和乘法運算幾乎與位運算同樣快,可是位運算消耗更少的資源。經常使用的位運算以下:html

  1. 位與 &

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

  1. 位或 |

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

  1. 位非 ~

( ~1=0 ~0=1)redis

  1. 位異或 ^

(1^1=0 1^0=1 0^0=0)算法

  1. 有符號右移 >>

在執行右移操做時,若參與運算的數字爲正數,則在高位補0;若爲負數,則在高位補1。shell

  1. 無符號右移 >>>

不管參與運算的數字爲正數或爲負數,在執運算時,都會在高位補0。數組

  1. 左移

對於左移是沒有正數跟負數這一說的,由於負數在CPU中是以補碼的形式存儲的,對於正數左移至關於乘以2的N次冪。安全

敲重點:上面的重重都是簡單的只是爲了引出下面的結論:數據結構

a % (Math.pow(2,n)) 等價於 a&( Math.pow(2,n)-1)多線程

好比a%16最終的結果必定是0~15之間的數字,而a&1111正好把a除16後的餘數有效的現實出來了由於若是是1 1111這樣的話最前面一位其實表明的16,也就是說二進制從倒數第五位開始只要出現了1那絕對錶明的是16的倍數。 結論:位運算比除法運算在運行效率上更高,對一個數取餘儘可能用a&二進制數這樣能夠更好的提速。

ArrayList

咱們知道ArrayList是一個數組隊列,至關於動態數組。與Java中的基本數組相比,它的容量能動態增加。它具備如下幾個重點。

  1. ArrayList 其實是經過一個 數組去保存數據的。當咱們構造ArrayList時;若使用默認構造函數,則ArrayList的默認容量大小是 10
  2. 當ArrayList容量不足以容納所有元素時,ArrayList會從新設置容量:原來容量的1.5倍。
  3. ArrayList的克隆函數,便是將所有元素 克隆到一個數組中。
  4. 克隆的底層是System.arraycopy(0,oldsrc,0,newsrc,length);
  5. ArrayList實現java.io.Serializable的方式。當寫入到輸出流時,先寫入「容量」,再依次寫入「每個元素」;當讀出輸入流時,先讀取「容量」,再依次讀取「每個元素」。

優勢:

  1. 根據下標遍歷元素效率較高。
  2. 根據下標訪問元素效率較高。
  3. 在數組的基礎上封裝了對元素操做的方法。
  4. 這樣的動態數組在內地地址上是空間連續的。
  5. 能夠自動擴容。

缺點:

  1. 插入和刪除的效率比較低。
  2. 根據內容查找元素的效率較低。
  3. 擴容規則:每次擴容現有容量的50%。

LinkedList

在這裏插入圖片描述 雙向鏈表每個節點包含三部分(data,prev,next),它不要求空間是連續的。相似於節點跟節點之間經過先後兩條線串聯起來的。

ArrayList和LinkedList總結:

  1. ArrayList是實現了基於動態數組的數據結構,LinkedList是基於鏈表結構。
  2. 對於隨機訪問的get和set方法,ArrayList要優於LinkedList,由於LinkedList要移動指針。
  3. 對於新增和刪除操做add和remove,LinkedList比較佔優點,由於ArrayList要移動數據。
  4. ArrayList使用在 查詢比較多,可是插入和刪除比較少的狀況,而LinkedList用在查詢比較少而插入刪除比較多的狀況

RedBlackTree

首先你須要對二叉樹有個瞭解,知道這是什麼樣子的一個數據組合方式,而後知道二叉樹查找的時候缺點,可能發生數據傾斜。所以引入了平衡二叉樹,平衡二叉樹的左右節點深度之差不會超過1,查找方便構建麻煩,所以又出現了紅黑樹。紅黑樹是一種平衡的二叉查找樹,是一種計算機科學中經常使用的數據結構,最典型的應用是實現數據的關聯,例如map等數據結構的實現,紅黑樹重要特性是( 左節點 < 根節點 < 右節點) 紅黑樹有如下限制:

  1. 節點必須是紅色或者是黑色
  2. 根節點是黑色的
  3. 全部的葉子節點是黑色的。
  4. 每一個紅色節點的兩個子節點是黑色的,也就是不能存在父子兩個節點全是紅色
  5. 從任意每一個節點到其每一個葉子節點的全部簡單路徑上黑色節點的數量是相同的。

若是您對紅黑樹還不太瞭解推薦看下博主之前寫的RBT

HashTable

Hash表是一種特殊的數據結構,它同數組、鏈表以及二叉排序樹等相比較有很明顯的區別,它可以快速定位到想要查找的記錄,而不是與表中存在的記錄的關鍵字進行比較來進行查找。這個源於Hash表設計的特殊性,它採用了==函數映射==的思想將記錄的存儲位置與記錄的關鍵字關聯起來,從而可以很快速地進行查找。評價函數的性能關鍵在於==裝填因子==,以及如何合理的解決哈希衝突,具體的可看博主之前寫的完全搞定哈希表

HashMap源碼剖析

概述

一般具有前面一些知識點的鋪墊就能夠很好的開展HashMap的講解了,既然ArrayListLinkedListRed Black Tree各有優缺點,咱們能不能集百家之長實現一個綜合產物呢 === >HashMap,本文因此講解都是基於JDK8。

HashMap的組成部分:數組 + 鏈表 + 紅黑樹。HashMap的主幹是一個Node數組。NodeHashMap的基本組成單元,每個Node包含一個key-value鍵值對。HashMap的時間複雜讀幾乎能夠接近O(1)(若是出現了 哈希衝突可能會波動下),而且HashMap的空間利用率通常就是在40%左右。HashMap的大體圖以下: 在這裏插入圖片描述 PS:其中幾個重要節點關係以下:

  1. java.util.Map.Entry 這就是個 interface定義了一些比較的接口函數。 在這裏插入圖片描述
  2. java.util.HashMap.Node 就是咱們 HashMap中存儲的基本的KV。 在這裏插入圖片描述
  3. java.util.LinkedHashMap.Enrty Enrty這個類繼承自 HashMap.Node這個類, EnrtyLIinkedHashMap的一個內部類, 在這裏插入圖片描述
  4. java.util.HashMap.TreeNode TreeNode的構造函數向上追溯繼承了 LinkedHashMap.Entry,然後者又繼承了 HashMap.Node。因此 TreeNode既保有 Node的屬性,同時因爲添加了 prev這個前驅指針使得==鏈表==變爲了==雙向==。前三個節點跟第五個紅黑樹相關,第四個跟 next跟雙向鏈表相關。 在這裏插入圖片描述 在這裏插入圖片描述 數據存儲的大體步驟有三步。
  5. 每一個數據經過 HashTable裏的映射函數來決定將該數據放到數組的那個地方,數組初始化時候必定是2的次冪,默認16,初始化傳入的任何數字都會通過 tableSizeFor調整爲2次冪。
  6. 若是同一個數組的地方被分配到太多數據就用 鏈表法來解決哈希衝突。
  7. 若是同一個節點的鏈表數據節點個數 > TREEIFY_THRESHOLD=8且數組長度 >= MIN_TREEIFY_CAPACITY=64,則會將該鏈表進化位 RedBlackTree,若是 RedBlackTree中節點個數小於 UNTREEIFY_THRESHOLD=6會退化爲鏈表。

特別提醒:讀HashMap源碼以前須要知道它大體特性以下:

  1. HashMap的存取是沒有順序的
  2. KV均容許爲NULL
  3. 多線程狀況下該類安全,能夠考慮用HashTable。
  4. JDk8底層是數組 + 鏈表 + 紅黑樹,JDK7底層是數組 + 鏈表。
  5. 初始容量和裝載因子是決定整個類性能的關鍵點,輕易不要動。
  6. HashMap是 懶漢式建立的,只有在你put數據時候纔會build
  7. 單向鏈表轉換爲紅黑樹的時候會先變化爲 雙向鏈表最終轉換爲 紅黑樹,雙向鏈表跟紅黑樹是 共存的,切記。
  8. 對於傳入的兩個 key,會強制性的判別出個高低,判別高低主要是爲了決定向左仍是向右。
  9. 鏈表轉紅黑樹後會努力將紅黑樹的 root節點和鏈表的頭節點 跟 table[i]節點融合成一個。
  10. 在刪除的時候是先判斷刪除節點紅黑樹個數是否須要轉鏈表,不轉鏈表就跟 RBT相似,找個合適的節點來填充已刪除的節點。
  11. 紅黑樹的 root節點 不必定table[i]也就是鏈表的頭節點是同一個哦,三者同步是靠 MoveRootToFront實現的。而 HashIterator.remove()會在調用 removeNode的時候 movable=false
在這裏插入圖片描述
在這裏插入圖片描述

重要參數

靜態參數
  1. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4

初始容量,默認容量=16,箱子的個數不能太多或太少。若是太少,很容易觸發擴容,若是太多,遍歷哈希表會比較慢。

  1. static final int MAXIMUM_CAPACITY = 1 << 30

數組的最大容量,通常狀況下只要內存夠用,哈希表不會出現問題

  1. static final float DEFAULT_LOAD_FACTOR = 0.75f

默認的負載因子。所以初始狀況下,當存儲的全部節點數 > (16 * 0.75 = 12 )時,就會觸發擴容。 默認負載因子(0.75)在時間和空間成本上提供了很好的折衷。較高的值會下降空間開銷,但提升查找成本(體如今大多數的HashMap類的操做,包括get和put)。設置初始大小時,應該考慮預計的entry數在map及其負載係數,而且儘可能減小rehash操做的次數。若是初始容量大於最大條目數除以負載因子,rehash操做將不會發生。

從上面的表中能夠看到當桶中元素到達8個的時候,機率已經變得很是小,也就是說用0.75做爲加載因子,每一個碰撞位置的鏈表長度超過8個是幾乎不可能的。 4. static final int TREEIFY_THRESHOLD = 8

這個值表示當某個箱子(數組的某個item)中,鏈表長度 >= 8 時,有可能會轉化成樹。設置爲8,是系統根據泊松分佈的數據分佈圖來設定的。

  1. static final int UNTREEIFY_THRESHOLD = 6

在哈希表擴容時,若是發現鏈表長度 <= 6,則會由樹從新退化爲鏈表。 設置爲6猜想是由於時間和空間的權衡

當鏈表長度爲6時 查詢的平均長度爲 n/2=3,紅黑樹 log(6) = 2.6 爲8時 :鏈表 8/2=4, 紅黑樹 log(8)=3

  1. static final int MIN_TREEIFY_CAPACITY = 64

鏈表轉變成樹以前,還會有一次判斷,只有數組長度大於 64 纔會發生轉換。這是爲了不在哈希表創建初期,多個鍵值對剛好被放入了同一個鏈表中而致使沒必要要的轉化。

動態參數
  1. transient Node<K,V>[] table

HashMap的鏈表數組。不管咱們初始化時候是否傳參,它在自擴容時老是2的次冪。

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

HashMap實例中的Entry的Set集合

  1. transient int size

HashMap表中存儲的實例KV個數。

  1. transient int modCount

凡是咱們作的增刪改都會引起modCount值的變化,跟版本控制功能相似,能夠理解成version,在特定的操做下須要對version進行檢查,適用於Fai-Fast機制。

在java的集合類中存在一種Fail-Fast錯誤檢測機制,當多個線程對同一集合的內容進行操做時,可能就會產生此類異常。 好比當A經過iterator去遍歷某集合的過程當中,其餘線程修改了此集合,此時會拋出ConcurrentModificationException異常。 此類機制就是經過modCount實現的,在迭代器初始化時,會賦值expectedModCount,在迭代過程當中判斷modCountexpectedModCount是否一致。

  1. int threshold

擴容閾值 threshold = capacity * loadFactor

  1. final float loadFactor

可自定義的負載因子,不過通常都是用系統自帶的0.75。

四種構造方法

  1. 默認構造方法 在這裏插入圖片描述
  2. 傳入初始容量大小 在這裏插入圖片描述
  3. 傳入初始容量大小及負載因子 在這裏插入圖片描述 ==tableSizeFor==:做用是返回大於輸入參數且最小的2的整數次冪的數。好比10,則返回16。 在這裏插入圖片描述 詳解以下:

先來分析有關n位操做部分:先來假設n的二進制爲01xxx...xxx。接着 對n右移1位:001xx...xxx,再位或:011xx...xxx 對n右移2爲:00011...xxx,再位或:01111...xxx 此時前面已經有四個1了,再右移4位且位或可得8個1 同理,有8個1,右移8位確定會讓後八位也爲1。

綜上可得,該算法讓最高位的1後面的位全變爲1。最後再讓結果n+1,即獲得了2的整數次冪的值了。 如今回來看看第一條語句:

int n = cap - 1;

讓cap-1再賦值給n的目的是另找到的目標值大於或等於原值。例如二進制1000,十進制數值爲8。若是不對它減1而直接操做,將獲得答案10000,即16。顯然不是結果。減1後二進制爲111,再進行操做則會獲得原來的數值1000,這種二進制方法的效率很是高。

  1. 構造函數傳入一個map 使用默認的負載因子,而後根據當前 map的大小來反推須要的 threshold,同時還可能會涉到 resize,而後住個 put到 容器中。
在這裏插入圖片描述
在這裏插入圖片描述

Hash值

不管咱們put數據仍是get數據都要先得到該數據在這個哈希表中對應的位置。好比put數據,它的流程分爲2步。

1.先得到key對應的hash值。 2. 將該數據的hash值A,跟將A右無符號移動16位後再^獲得最終值。這個操做叫擾動,緣由是怕低幾位出現想同的機率太大,儘量的將數據實現均勻分佈

在這裏插入圖片描述
在這裏插入圖片描述

同時JDK8跟JDK7的擾動目的同樣,不過複雜程度不同在這裏插入圖片描述

1. get

相對來講很簡單,爲方便理解先說下代碼大體流程思路。

  1. 得到key的hash而後根據hash和key按照插入時候的思路去查找 get
  2. 若是數組位置爲NULL則直接返回 NULL。
  3. 若是數組位置不爲NULL則先直接看數組位置是否符合。
  4. 若是數組位置有類型說紅黑樹類型,則按照紅黑樹類型查找返回。
  5. 若是數組有next,則按照遍歷鏈表的方式查找返回。
在這裏插入圖片描述
在這裏插入圖片描述

1.1 getNode

宏觀查找函數細節: 在這裏插入圖片描述

1.3 getTreeNode

紅黑樹查找節點細節:

  1. 先得到根節點,左節點,右節點。
  2. 根據 左節點 < 根節點 < 右節點 對對數據進行逐步範圍的縮小查找。
  3. 若是實現了Comparable方法則直接對比。
  4. 不然若是根節點不符合則遞歸性的調用find查找函數。
在這裏插入圖片描述
在這裏插入圖片描述

1.4 ComparableClassFor:

查詢該key是否實現了Comparable接口。 在這裏插入圖片描述

1.5 compareComparables:

既然實現了Comparable接口就用該實現進行對比判斷如何何去何從。 在這裏插入圖片描述

2. put流程

跟隨源碼梳理下put操做的大體流程。 在這裏插入圖片描述 數據插入的時候大體流程以下:

  1. 對數據進行 Hash值計算。
  2. 將數據插入前先查看下當前 table的狀態,若是 table是空須要調用 resize來進行初始化。
  3. 經過位運算得到 key的目標位置。並判斷當前位置狀況。
  4. 若是當前位置爲空則直接進行放置,若是跟當前key一直則進行覆蓋。
  5. 若是當前有數據則看當前數據類型是不是紅黑樹,是的話須要調用 putTreeVal
  6. 不然就認爲是個鏈表,而後循環的查找進行尾部==插入==。同時還要考慮當前鏈表轉紅黑樹。

在JDK8中尋找待插入點 e是經過==尾插法==(相似與排隊在最後面),而在JDK7中是==前插法==(相似與加塞在最前面,之因此這樣作是HashMap發明者認爲後插入節被訪問機率更大),對應代碼以下。

在這裏插入圖片描述
在這裏插入圖片描述
  1. 對找到的舊節點 e進行判斷
  1. oldValue對應的舊值若是爲NULL,那麼不管onlyIfAbsent是否決定替換。都將被替換。
  2. oldValue對應的舊值若是不爲NULL,那麼若是onlyIfAbsent是false就替換。
  3. onlyIfAbsent:只有在缺席的狀況下才替換,不缺席不替換。跟redis Setnx 一樣的功能。
  1. 數據最終添加完畢後要對對修改後的變量 modCount加1,同時看最新的總的節點數是否須要擴容了,若是是就擴容。

2 put

在這裏插入圖片描述
在這裏插入圖片描述

2.1 putTreeVal

  1. 先找到根節點,而後判斷是從左邊找仍是右邊找key。
  2. 找到了則直接返回找到的節點。
  3. 沒找到則新建節點將該新建節點放到適當的位置,同時考慮紅黑樹跟雙向鏈表的節點插入狀況。 在這裏插入圖片描述

2.2 treeifyBin

主要功能是根據參數的閾值範圍絕對是否將鏈表轉化爲紅黑樹,而後首先將單項鍊表轉化爲雙向鏈表,再調用treeify以頭節點爲根節點構建紅黑樹。 在這裏插入圖片描述

2.3 treeify

雙向鏈表跟紅黑樹建立,主要步驟分3步。

  1. 從該雙向鏈表中找第一個節點爲 root節點。
  2. 每個雙向鏈表節點都在root節點爲根都二叉樹中找位置而後將該數據插入到紅黑樹中,同時要注意 balance
  3. 最終要注意將根節點跟當前 table[i]對應好。 ###

2.4 moveRootToFront

確保將root節點挪動到table[first]上,若是紅黑樹構建成功而沒成功執行這個任務會致使tablle[first]對應的節點不是紅黑樹的root節點。正常執行的時候主要步驟分2步。

  1. 找到跟節點而後將 root節點放到跟節點,至此關於紅黑樹到操做搞定。
  2. 原來鏈表頭是 first節點,如今將多是中間節點的 root節點挪到 first節點前面。 在這裏插入圖片描述 其中 checkInvariants函數的做用:校驗 TreeNode對象是否知足紅黑樹和雙鏈表的特性。由於併發狀況下會發生不少異常。

3 resize

  1. 得到老table數據,若是老table已經足夠大則再也不擴容,只調節閾值。
  2. 老table擴容後的範圍也 符合要求直接將容器大小跟閾值都擴容 。
  3. 若是是帶參數構造函數則須要將閾值複製給容器容量。
  4. 不然認爲該容器初始化時未傳參,需初始化。
  5. 若是老table有數據,新他變了大小設置好了可是閾值沒設置成功。此時要設置新閾值。
  6. 建立新容器。
  7. 老table成功擴容爲新table,涉及到數據的轉移。
  1. 數據不爲空是單獨的節點則直接從新hash分配新位置。
  2. 數據不爲空後面是一個鏈表,則要把鏈表數據進行區分看那些分到老地方那些分到新地方。
  3. 若是該節點類型是個紅黑樹則調用split.

在這裏插入圖片描述 鏈表形式的從新劃分解釋以下: 注意:不是(e.hash & (oldCap-1))而是(e.hash & oldCap), 後一個獲得的是 元素的在數組中的位置是否須要移動,示例以下

示例1: e.hash= 10 0000 1010 oldCap=16 0001 0000 & =0 0000 0000 比較高位的第一位 0 結論:元素位置在擴容後數組中的位置沒有發生改變 示例2: e.hash= 17 0001 0001 oldCap=16 0001 0000 & =1 0001 0000 比較高位的第一位 1 結論:元素位置在擴容後數組中的位置發生了改變,新的下標位置是原下標位置+原數組長度

在這裏插入圖片描述
在這裏插入圖片描述

3.1 split

擴容後如何處理原來一個table[i]上的紅黑樹,代碼的總體思路跟處理鏈表的時候差很少,只要理解節點關係保存紅黑樹的時候也保存了雙向鏈表就OK了。 在這裏插入圖片描述

4. find

函數功能就是以指定的一個節點爲根節點,根據指定的keyvalue進行查找。

  1. 經過hash值判斷 左邊找仍是右邊找。
  2. 若是找到的很簡單直接返回。
  3. 可能出現hash值相等但是 key不同,繼續查找分爲三種狀況。
  1. 左節點爲空則找右節點
  2. 右節點爲空則找左節點
  3. 左右節點都不會空,嘗試通 Comparable對數據看向左仍是向右。
  4. 沒法經過comparable比較或者比較以後仍是相等。
  1. 直接從右節點遞歸查找下。
  2. 不然就從左邊查找。
在這裏插入圖片描述
在這裏插入圖片描述

4.1 tieBreakOrder

對兩個對象進行比較,必定能比出個高低。

  1. a 跟 b 都是字符串則直接在if判斷裏比拼完畢
  2. a 跟 b 都是對象則直接查看對象在JVM中的hash地址,而後比較。
在這裏插入圖片描述
在這裏插入圖片描述

4. remove

函數入口而已: 在這裏插入圖片描述

4.1 removeNode

removeNode無非就是查看table[i]是否存在,而後是否在首節點上,是否在紅黑樹上,是否在鏈表上。這幾種狀況,找到了則直接刪除,同時注意平衡性。 在這裏插入圖片描述

4.2 removeTreeNode

該函數的 目的就是移除調用此方法的節點,也就是該方法中的this節點。移除包括鏈表的處理和紅黑樹的處理。能夠看之前寫過的RBT,刪除的時候思路大體是同樣的,這裏大體分爲3步驟。

  1. 紅黑樹也是雙向鏈表,以鏈表的角度來刪除節點,而後判斷是否須要退化爲鏈表。
  2. 根據當前的 p節點嘗試從 pr找最 的或者從 pl找最 的目標節點 s,將兩點兌換。
  3. 找到要 replacement來跟 p進行替換。
  4. 實施替換。
  5. 替換後爲保持紅黑樹特性可能須要進行 balance
在這裏插入圖片描述
在這裏插入圖片描述

4.3 untreeify

紅黑樹退化成鏈表 在這裏插入圖片描述

4.4 balanceDeletion

關於這個問題能夠直接看博主之前寫的紅黑叔添加跟刪除RBT

JDK7死環問題

JDK7對舊table數據重定位到新table的函數transfer以下,其中重點關注部分以標出。 在這裏插入圖片描述

  1. 頭插法正常狀況下: 在這裏插入圖片描述
  2. 併發狀況下 線程1只執行了 Entry<K,V> next = e.next就被掛起了,而線程2正常執行完畢,結果圖以下: 在這裏插入圖片描述 線程1接着下面繼續執行: 在這裏插入圖片描述 經過逐步分析跟繪圖能夠知道 會有環產生。

HashIterator 的 remove 方法

7vs8

  1. 7中找 Hash用了4次,8中只用了1次。
  2. 7 = 數組 + 鏈表,8 = 數組 + 鏈表 + 紅黑樹
  3. 7中是頭插法,多線程容易形成環,8中是尾插法。
  4. 7的擴容是所有數據從新定位,8中是位置不變+ 移動舊size大小來實現更好些。
  5. 7是先判斷是否要擴容再插入,8中是先插入再看是否要擴容。
  6. HashMap無論78都是現場不安全的,多線程狀況下記得用 ConcurrentHashmapConcurrentHashmap下篇文章說。

常見問題

隨機蒐羅了一些常見HashMap問題,若是把上述代碼都看懂了應付這些應該沒問題。

  1. HashMap原理,內部數據結構。
  2. HashMap中的put,get,remove大體過程。
  3. HashMap中 hash函數實現。
  4. HashMap如何擴容。
  5. HashMap幾個重要參數爲何這樣設定。
  6. HashMap爲何線程不安全,如何替換。
  7. HashMap在JDK7跟JDK8中的區別。
  8. HashMap中鏈表跟紅黑樹切換思路。
在這裏插入圖片描述
在這裏插入圖片描述

參考

HashMap講解 HashMap詳解 疫苗JAVA HASHMAP的死循環

本文使用 mdnice 排版

相關文章
相關標籤/搜索