1. 程式人生 > >HashMap實現原理(jdk1.7/jdk1.8)

HashMap實現原理(jdk1.7/jdk1.8)

HashMap的底層實現: 

1、簡單回答 

  JDK1.7:HashMap的底層實現是:陣列+連結串列
  JDK1.8:HashMap的底層實現是:陣列+連結串列/紅黑樹
  
  為什麼要紅黑樹?
  紅黑樹:一個自平衡的二叉樹
  當結點多了用紅黑樹,少了用連結串列
  因為少的話用紅黑樹太複雜,多了話用紅黑樹可以提高查詢效率。
  紅黑樹:(自動調整根結點,保證左右兩邊的結點數差不多),它會左旋,右旋來實現。


   

 JDK1.7:HashMap的底層實現是:陣列+連結串列

2、複雜回答v1.0版

 
 每一個對對映關係的儲存位置:
  儲存的位置:
(1)先計算key的hash值,通過hashCode()就可以得到,
(2)再用hash值經過各種(異或)操作,得到一個int數字,目的是使得更均勻的分佈在陣列的每一個元素中。
(3)再用剛才的int值,與table陣列的長度-1做一個“按位與&"
運算,目的是得到一個[i]
    因為陣列的長度是2的n次方,長度-1的數的二進位制是前面都是0,後面都是1,任何數與它做“&”,結果一定是[0,該數]之間
 
 
 為什麼會出現連結串列?
  (1)兩個不同的key物件,仍然可能出現hashCode值一樣
  (2)就算hashCode不一樣,經過剛才的計算,得到[i]是一樣的
  (3)而我們的陣列的元素table[i]本來只能放一個元素,那麼現在有多個元素需要儲存到table[i]中,只能把這幾個元素用連結串列連線起來
 
   簡單比喻:
   y = f(x)
  兩個不一樣的x,可能得到一樣的y
 
 那麼儲存到HashMap中的是什麼樣的元素呢?
  (1)首先是Map.Entry型別:對映項(鍵-值對)。
  (2)其次HashMap有一個內部類HashMap.Entry實現了Map.Entry介面型別
 
  內部類Entry由四部分組成:
  (1)key
  (2)value
  (3)下一個Entry:next
  (4)hash值計算的整數值,為了後面查詢快一點
 
 
  如何避免重複?
  如果兩個key的hash值是一樣的,還要呼叫一下equlas()方法,如果返回true,就會把新的value替換舊的value。
  如果兩個key的hash值是一樣的,還要呼叫一下equlas()方法,如果返回false,就會把新的Entry連線到舊的Entry所在連結串列的頭部(first)
 如果兩個key的hash值是不一樣的,但是[i]是一樣的,就會把新的Entry連線到舊的Entry所在連結串列的頭部(first)
  如果兩個key的hash值是不一樣的,並且[i]不一樣的,肯定存在不同table[i]中
 
  我們把table[i]稱為“桶bucket”。
 
 
  回憶:兩個物件的hash值:
  (1)如果兩個物件是“相等”,他們的hash值一定是一樣
   (2)如果兩個物件的hash值是一樣,他們可能是相同的物件,也可能是不同的物件。
 


  3、複雜追蹤原始碼v2.0版


  (1)什麼時候擴容
  當元素的個數達到“閾值”,並且新新增的對映關係計算出來的table[i]位置是非空的情況下,再擴容
  預設載入因子 (0.75):DEFAULT_LOAD_FACTOR
  閾值 = table.length * 載入因子(loadFactor)
  第一次閾值:16 * 0.75 = 12
  初始容量:DEFAULT_INITIAL_CAPACITY:16
  第二次閾值:32 * 0.75 = 24
  ...

 HashMap中table的長度有一個要求:必須是2的n次方
 
 
  (2)跟蹤一下put方法的原始碼
  第一步:如果table是空的,會先把table初始化為一個長度為16的陣列,如果你指定的長度不是2的n次方,會往上糾正為最接近的2的n次方
          並且把閾值 threshold= table.length * 載入因子(loadFactor) = 12。
  第二步:如果key是null,首先確定的位置是table[0],
     如果原來table[0]已經有key為null的Entry,用新的value替換舊的value
   如果原來table[0]沒有key為null的Entry,那麼建立一個新的Entry物件,作為table[0]桶下面的連結串列的頭,原來的那些作為它next。
 
  第三步:如果key不是null,那麼用key的hashCode值,通過hash()函式算出一個int值稱為"hash"
  第四步:通過剛才的“hash”的int值與table.length-1做&運算,得到一個下標index,表示新的對映關係即將存入table[index]
  第五步:迴圈判斷table[index]位置是否為空,並且是否有Entry的key與我新新增的key是否“相同”,如果相同,就用新的value替換舊的value
  第六步:新增新的對映關係
    (1)判斷是否要擴容:        
        當元素的個數達到“閾值”,並且新新增的對映關係計算出來的table[i]位置是非空的情況下,table再擴容為原來的2倍長
        如果擴容了,那麼要重新計算hash和index
    (2)把新的對映關係建立為一個Entry的物件放到table[index]的頭部。

 

 

 JDK1.8:HashMap的底層實現是:陣列+連結串列/紅黑樹 


  1、複雜回答v1.0


  (1)對映關係的型別
 新增到HashMap1.8種的對映關係的物件是HashMap.Node型別,或者是HashMap.TreeNode型別。
  它也是Map.Entry介面的實現類。
 
 (2)對映關係新增到table[i]中時,如果裡面是連結串列,新的對映關係是作為原來連結串列的尾部
  “七上八下”:JDK1.7在上,JDK1.8在下。
  
  為什麼要在下面,因為如果是紅黑樹,那麼是在葉子上,保持一致,都在下面。
 
 (3)擴容的時機不同
  第一種擴容:元素個數size達到閾值threshod = table.length * 載入因子  並且table[i]是非空的
 第二種擴容:當某個桶table[index]下面的結點的總數原來已經達到了8個,這個時候,要新增第9個時,會檢查
  table.length是否達到64,如果沒達到就擴容。如果新增第10個時,也會檢查table.length是否達到64,如果沒達到就擴容。
 為什麼,因為一旦擴容,所有的對映關係要重新計算hash和index,一計算原來table[index]下面可能就沒有8個,或新的對映關係也可能不在table[index],
  這樣就可能均勻分佈。
 
 (4)什麼時候從連結串列變成紅黑樹
 當table[index]下面結點的總數已經達到8個,並且table.length也已經達到64,那麼再把對映關係新增到
  table[index]下時,就會把原來的連結串列修改紅黑樹
 
  (5)什麼時候會從紅黑樹變為連結串列
  當刪除對映關係時table[index]下面結點的總數少於6個,會把table[index]下面的紅黑樹變回連結串列。


2、put的原始碼v2.0


 第一步:計算key的hash,用了一個hash()函式,目的得到一個比hashCode更合理分佈的一個int值
 第二步:如果table是空的,那麼把table初始化為長度為16的陣列,閾(yu)值初始化為= 預設的長度16 * 預設的載入因子0.75 = 12
 DEFAULT_INITIAL_CAPACITY:預設的初始化容量16
 DEFAULT_LOAD_FACTOR:預設載入因子 0.75
 第三步:檢視table[index]是否為空,如果為空,就直接new一個Node放進去
     index = hash的int值 & table.length-1
  第四步:先檢視table[index]的根節點的key是否與新新增的對映關係的key是否“相同”,如果相同,就用新的value替換原來的value。
   第五步:如果table[index]的根節點的key與新新增的對映關係的key不同,
  還要看table[index]根結點的型別是Node還是TreeNode型別,
  如果是Node型別,那麼就檢視連結串列下的所有節點是否有key與新新增的對映關係的key是否“相同”,如果相同,就用新的value替換原來的value。
  如果是TreeNode型別,那麼就檢視紅黑樹下的所有葉子節點的key是否與新新增的對映關係的key是否“相同”,如果相同,就用新的value替換原來的value。
  
 第六步:如果沒有找到table[index]有結點與新新增的對映關係的key“相同”,那麼
 如果是TreeNode型別,那麼新的對映關係就建立為一個TreeNode,連線到紅黑樹中
 如果是Node型別,那麼要檢視連結串列的結點的個數,是否達到8個,如果8個,並且table.length小於64,那麼先擴容。
 TREEIFY_THRESHOLD:樹化閾值8
 MIN_TREEIFY_CAPACITY:最小樹化容量64


 UNTREEIFY_THRESHOLD:反樹化,從數變為連結串列的閾值6