Java HashMap總結

HashMap的數據結構

底層由數組和鏈表組成,數組是主體,鏈表是爲了解決Hash衝突,jdk1.8後,當鏈表的長度大於閾值8時,會轉換爲紅黑樹java

HashMap的hash函數實現原理

一、JDK 1.8 中,經過key的hashCode()方法獲得值的高16位異或低16 位實現的:程序員

(h = k.hashCode()) ^ (h >>> 16)

二、使用計算獲得的hash值與數組長度n-1作與位運算獲得數組下標:數組

if ((p = tab[i = (n - 1) & hash]) == null)

計算下標時爲何用位與運算?安全

  • 因爲位運算直接對內存數據進行操做,不須要轉成十進制,所以處理速度很是快
  • 正整數對2的倍數取模,只要將數與2的倍數-1作位與運算
  • 對2的倍數取餘,只要將數右移2的倍數位

爲何使用異或運算?
這段代碼叫「擾動函數」,目的是爲了混合原hash碼的高位和地位,混合後的低位摻雜了高位的部分特徵,這樣高位的信息也被變相的保留下來session

  • 計算獲得hash值後須要與數組長度-1作與位運算獲得元素所在數組下標位置,此時數組長度-1至關於一個「低位掩碼」,結果是hash值的高位所有歸零,只保留低位值
  • hashCode函數返回的int值範圍爲32位,右移16位再異或,至關於本身的高半區和低半區作異或,這樣獲得的hash值混合了原始hash碼高位和低位的特徵,減小了hash碰撞的概率

爲何用異或,不用與和或運算?
image
圖片轉自:https://zhuanlan.zhihu.com/p/...數據結構

HashMap的put方法執行過程

  • 調用哈希函數獲取Key對應的hash值,再計算其數組下標;
  • 若是沒有出現哈希衝突,則直接放入數組;若是出現哈希衝突,則以鏈表的方式放在鏈表後面;
  • 若是鏈表長度超過閥值( TREEIFY THRESHOLD==8),就把鏈表轉成紅黑樹,鏈表長度低於6,就把紅黑樹轉回鏈表;
  • 若是結點的key不存在,直接插入,若是已經存在,則替換其value;
  • 若是集合中的鍵值對大於12,調用resize方法進行數組擴容。

HashMap的容量與擴容機制

初始容量與負載因子:函數

  • HashMap默認初始容量爲16,負載因子爲0.75性能

    • 負載因子爲0.75是在容量浪費和hash衝突增多之間取的一個值
  • 指定初始容量時推薦使用2的倍數做數組長度優化

    • 由於只有2的倍數在減1的時候,纔會出現01111這樣的值,才能用來作與位運算替代取模運算
    • 若是指定的初始容量不爲2的倍數,則會尋找比原始值大的最小的那個2的倍數值,如傳17,則初始容量爲32

擴容過程:spa

  • 建立一個新數組,其容量爲舊數組的兩倍
  • 將原數組中的元素遷移至新數組。jdk1.8如下須要從新計算下標位置,jdk1.8後作了優化,只需計算hash & oldCap的值便可肯定元素在新數組中的位置(oldCap爲原數組長度),分如下兩張狀況:

A:擴容後,若hash值新增參與運算的位=0,那麼元素在擴容後的位置=原始位置
B:擴容後,若hash值新增參與運算的位=1,那麼元素在擴容後的位置=原始位置+原數組長度。

image

因此說擴容是一個特別耗性能的操做,因此當程序員在使用HashMap的時候,估算map的大小,初始化的時候給一個大體的數值,避免map進行頻繁的擴容。

爲何使用須要鏈表轉紅黑樹

  • 引入紅黑樹就是爲了查找數據快,解決鏈表查詢深度的問題。鏈表查詢效率爲O(n),當長度過長時查詢效率慢,紅黑樹查詢效率O(lgn),查詢效率快
  • 紅黑樹在插入新數據後可能須要經過左旋,右旋、變色這些操做來保持樹的平衡
  • 構建紅黑樹和維護紅黑樹的平衡也須要一些操做損耗資源,因此在鏈表長度大於8時才轉換成紅黑樹

紅黑樹:

  • 每一個節點非紅即黑
  • 根節點老是黑色的
  • 若是節點是紅色的,則它的子節點必須是黑色的(反之不必定)
  • 每一個葉子節點都是黑色的空節點(NIL節點)
  • 從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)

jdk8中對HashMap作了哪些改變?

  • 在java 1.8中,若是鏈表的長度超過了8,那麼鏈表將轉換爲紅黑樹。
  • 發生hash碰撞時,java 1.7 會在鏈表的頭部插入,而java 1.8會在鏈表的尾部插入
  • 擴容遷移數據時,jdk1.8沒有從新計算新數組下標,而是經過hash & 原數組長度來肯定元素在新數組中的下標
  • 在java 1.8中,Entry被Node替代(換了一個馬甲)。

hash衝突的解決方法

一、開放地址法
當出現hash衝突時,執行左右空節點探測

  • 線性探測再散列,當hash值p出現衝突時,則將數據放到 (p + 1) % m處 ,逐步向後探測
  • 二次探測再散列,當hash值p出現衝突時,則將數據放到 (p + 1) % m 處,若是此時還存在衝突,則將數據放到 (p - 1) % m 處,左右探測
  • 僞隨機探測再散列,構建一個隨機數列探測,di依次取數列中的值

缺點:不能刪除節點、只能對節點做刪除標記;要求哈希表空間大於或等於裝填數據數目。

二、再hash法
構造多個hash函數,當一個衝突時,再計算第二個的hash值,直到不衝突爲止

優缺點:這種方式不容易產生彙集,但增長了計算時間

三、鏈地址法
出現衝突時用鏈表連接

缺點:須要額外的空間

四、創建公共溢出區
將哈希表分爲基本表和溢出表兩部分,凡是和基本表中元素髮生衝突的元素均存入溢出表

ConcurrentHashMap數據結構

jdk1.7:

  • 使用一個Segment數組和多個HashEntry數組組成
  • 當執行put操做時,會進行第一次key的hash來定位Segment數組的位置,若是該Segment尚未初始化,即經過CAS操做進行賦值,而後進行第二次hash操做,找到對應的HashEntry的位置,再經過ReentrantLock進行加鎖後,將數據添加到鏈表尾部

jdk1.8:

  • 與HashMap結構一致,底層是數組+鏈表或數組加紅黑樹的結構實現,當鏈表的節點數大於8時會轉換爲紅黑樹
  • 操做流程是,對key進行hash運算定位到數組下標,若是下標位置table爲空則先初始化,再cas插入,若是有數據,則用同步鎖Synchronized進行加鎖後插入

jdk1.7數據結構圖:
image

HashMap,LinkedHashMap,TreeMap 有什麼區別?

  • HashMap 經過hash函數計算數組下標,key是無序的
  • LinkedHashMap經過雙向鏈表保存了記錄的插入順序,在用 Iterator 遍歷時,先取到的記錄確定是先插入的;遍歷比 HashMap 慢;
  • TreeMap 實現 SortMap 接口,可以把它保存的記錄根據鍵排序(默認按鍵值升序排序,也~~~~能夠指定排序的比較器)

HashMap、TreeMap、LinkedHashMap的使用場景

  • HashMap:通常狀況下,使用最多的是 HashMap。只須要普通的在 Map 中插入、刪除和定位元素時;
  • TreeMap:在須要按天然順序或自定義順序遍歷鍵的狀況下;
  • LinkedHashMap:在須要輸出的順序和輸入的順序相同的狀況下。

HashMap 和 HashTable 有什麼區別?

  • HashMap 是線程不安全的,HashTable 是線程安全的;
  • HashMap容許插入null鍵,HashTable不容許;
  • HashMap 默認初始化數組的大小爲16,HashTable 爲 11,前者擴容時,擴大兩倍,後者擴大兩倍+1;
  • HashMap 須要從新計算 hash 值,而 HashTable 直接使用對象的 hashCode
相關文章
相關標籤/搜索