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碰撞的概率
爲何用異或,不用與和或運算?
圖片轉自:https://zhuanlan.zhihu.com/p/...數據結構
HashMap的put方法執行過程
- 調用哈希函數獲取Key對應的hash值,再計算其數組下標;
- 若是沒有出現哈希衝突,則直接放入數組;若是出現哈希衝突,則以鏈表的方式放在鏈表後面;
- 若是鏈表長度超過閥值( TREEIFY THRESHOLD==8),就把鏈表轉成紅黑樹,鏈表長度低於6,就把紅黑樹轉回鏈表;
- 若是結點的key不存在,直接插入,若是已經存在,則替換其value;
- 若是集合中的鍵值對大於12,調用resize方法進行數組擴容。
HashMap的容量與擴容機制
初始容量與負載因子:函數
擴容過程:spa
- 建立一個新數組,其容量爲舊數組的兩倍
- 將原數組中的元素遷移至新數組。jdk1.8如下須要從新計算下標位置,jdk1.8後作了優化,只需計算hash & oldCap的值便可肯定元素在新數組中的位置(oldCap爲原數組長度),分如下兩張狀況:
A:擴容後,若hash值新增參與運算的位=0,那麼元素在擴容後的位置=原始位置
B:擴容後,若hash值新增參與運算的位=1,那麼元素在擴容後的位置=原始位置+原數組長度。
因此說擴容是一個特別耗性能的操做,因此當程序員在使用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數據結構圖:
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