如今不少公司面試都喜歡問java的HashMap原理,特在此整理相關原理及實現,主要仍是由於不少開發集合框架都不甚理解,更不要說各類其餘數據結構了,因此形成面子造飛機,進去擰螺絲。html
哈希表做爲一種優秀數據結構 本質上存儲結構是一個數組,輔以鏈表和紅黑樹 數組結構在查詢和插入刪除複雜度方面分別爲O(1)和O(n) 鏈表結構在查詢和插入刪除複雜度方面分別爲O(n)和O(1) 二叉樹作了平衡 二者都爲O(lgn) 而哈希表二者都爲O(1)
哈希表本質是一種(key,value)結構 由此咱們能夠聯想到,能不能把哈希表的key映射成數組的索引index呢? 若是這樣作的話那麼查詢至關於直接查詢索引,查詢時間複雜度爲O(1) 其實這也正是當key爲int型時的作法 將key經過某種作法映射成index,從而轉換成數組結構
1.使用hash算法計算key值對應的hash值h(默認用key對應的hashcode進行計算(hashcode默認爲key在內存中的地址)),獲得hash值 2.計算該(k,v)對應的索引值index 索引值的計算公式爲 index = (h % length) length爲數組長度 3.儲存對應的(k,v)到數組中去,從而造成a[index] = node<k,v>,若是a[index]已經有告終點 便可能發生碰撞,那麼須要經過開放尋址法或拉鍊法(Java默認實現)解決衝突
固然這只是一個簡單的步驟,只實現了數組 實際實現會更復雜
hash表 數組相似下圖java
索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
--- | null | null | <10,node1> | <27,node2> | null | null | null | null |
--- |
jdk 1.7以及以前的結構相似以下:node
jdk 8中的結構以下:面試
兩個重要概念算法
h 經過hash算法計算獲得的的一個整型數值 h能夠近似看作一個由key的hashcode生成的隨機數,區別在於相同的hashcode生成的h必然相同 而不一樣的hashcode也可能生成相同h,這種狀況叫作hash碰撞,好的hash算法應儘可能避免hash碰撞 (ps:hash碰撞只能儘可能避免,而沒法杜絕,因爲h是一個固定長度整型數據,原則上只要有足夠多的輸入,就必定會產生碰撞) 關於hash算法有不少種,這裏不展開贅述,只須要記住h是一個由hashcode產生的僞隨機數便可 同時須要知足key.hashcode -> h 分佈儘可能均勻(下文會解釋爲什麼須要分佈均勻) 能夠參考https://blog.csdn.net/tanggao1314/article/details/51457585
由上咱們能夠知道,不一樣的hashcode可能致使相應的h即發生碰撞
那麼咱們須要把相應的<k,v>放到hashmap的其餘存儲地址數組
經過在數組以某種方式尋找數組中空餘的結點放置 基本思想是:當關鍵字key的哈希地址p=H(key)出現衝突時 以p爲基礎,產生另外一個哈希地址p1,若是p1仍然衝突,再以p爲基礎,產生另外一個哈希地址p2,…,直到找出一個不衝突的哈希地址pi ,
經過引入鏈表 數組中每個實體存儲爲鏈表結構,若是發生碰撞,則把舊結點指針指向新鏈表結點,此時查詢碰撞結點只須要遍歷該鏈表便可 在這種方法下,數據結構以下所示 int類型數據 hashcode 爲自身值
1.爲何須要擴容?擴容因子大仍是小好?bash
因爲數組是定長的,當數組儲存過多的結點時,發生碰撞的機率大大增長,此時hash表退化成鏈表數據結構
過大的擴容因子會致使碰撞機率大大提高,太小擴容因子會形成存儲浪費,在Java中默認爲0.75多線程
2.當從哈希表中查詢數據時,若是key對應一條鏈表,遍歷時如何判斷是否應該覆蓋?框架
當遍歷鏈表時,若是兩個key.hashcode的h一致會調用equals()方法判斷是否爲同一對象,equal的默認實現是比較二者的內存地址
所以爲何Java強調當重寫equals()時須要同時重寫hashcode()方法,假設兩個不一樣對象,在內存中的地址不一樣分別爲a和b,那麼重寫equals()之後a.equals(b) =true 開發者但願把a,b這兩個key視做徹底相等
然而因爲內存地址的不一樣致使hashcode不一樣,會致使在hashmap中儲存2個本應相同的key值
這裏提供一個範例
public class Student { //學號 public int student_no; //姓名 public String name; @Override public boolean equals(Object o) { Student student = (Student) o; return student_no == student.student_no; } }
一般狀況下咱們像上圖同樣指望經過判斷兩個Student的學號是不是否爲同一學生
然而在使用map或set集合時產生出乎意料的結果
當咱們重寫hashcode()時
@Override public int hashCode() { return Objects.hash(student_no); }
能夠看到如今能夠正常使用集合框架中的一些特性
3.爲何在HashMap中數組的長度length = 2^n(初始值爲16),即2的n次 ?
當計算索引值index = h % length 因爲計算機的取餘操做速度很慢,而計算機的按位取餘 & 的操做很是快,又由於 h%length = h & (length-1) (須要知足length = 2^n) 所以規定了length = 2^n 加快index的計算速度,所以是利用了計算機自己的計算特性
4.HashMap的紅黑樹在哪裏體現呢?
紅黑樹是JDK8中對hashmap做的一個變動,在JDK8以前,HashMap、HashSet採用數組+鏈表的形式來解決哈希衝突,咱們知道優秀的hash算法應避免碰撞的發生,但假如開發者使用了不合適的hash算法,O(1)級別的數組查詢會退化到O(n)級鏈表查詢,所以在JDK8中引入紅黑樹的,當一個結點的鏈表長度大於8時,鏈表會轉換成紅黑樹,提升查詢效率,而鏈表長度小於6時又會退化成鏈表
5.擴容是如何觸發的?
當hashmap中的size > loadFactory * capacity即會發生擴容,size 也是數組結點和鏈表結點的總和,要明確擴容是一個很是耗費性能的操做,由於數組的長度發生改變,須要對全部結點的索引值從新進行計算,而在JDK8中對這部分進行了優化,詳細能夠參考https://blog.csdn.net/aichuanwendang/article/details/53317351,在擴容完後減輕了碰撞產生的影響。可是值得注意的是若是兩個線程都發現HashMap須要從新調整大小了,它們會同時試着調整大小。在調整大小的過程當中,存儲在鏈表中的元素的次序會反過來,由於移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是爲了不尾部遍歷(tail traversing)。若是條件競爭發生了,那麼就死循環了。因此多線程環境要使用ConcurrentHashMap而不能使用HashMap。
在jdk 8中,對擴容進行了優化,增長了高16位異或低16位,此時當n變爲2倍時,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。若是沒有變,意味着不少不須要移動,具體可參見源代碼中hash方法的實現,也能夠參考https://my.oschina.net/u/2307589/blog/1800587的示意圖,畫的很清晰。
在正常的Hash算法下,紅黑樹結構基本不可能被構造出來,根據機率論,理想狀態下哈希表的每一個箱子中,元素的數量遵照泊松分佈,通俗易懂的解釋泊松分佈
(即除非hash算法有問題,不然單位時間內發生衝撞的機率是能夠估算出來的):
P(X=k) = (λ^k/k!)e^-λ,k=0,1,...
當負載因子爲 0.75 時,上述公式中 λ 約等於 0.5,所以箱子中元素個數和機率的關係以下:(參考https://blog.csdn.net/Philm_iOS/article/details/81200601),下述分佈來自源碼文檔
> * Because TreeNodes are about twice the size of regular nodes, we * use them only when bins contain enough nodes to warrant use * (see TREEIFY_THRESHOLD). And when they become too small (due to * removal or resizing) they are converted back to plain bins. In * usages with well-distributed user hashCodes, tree bins are * rarely used. Ideally, under random hashCodes, the frequency of * nodes in bins follows a Poisson distribution * (http://en.wikipedia.org/wiki/Poisson_distribution) with a * parameter of about 0.5 on average for the default resizing * threshold of 0.75, although with a large variance because of * resizing granularity. Ignoring variance, the expected * occurrences of list size k are (exp(-0.5) * pow(0.5, k) / * factorial(k)). The first values are: * * 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006 * more: less than 1 in ten million
最後和JDK 7不一樣的是,JDK1.8中新增了一個實現了Entry接口的內部類Node<K,V>,即哈希節點。
參考:
JDK 8中hashmap的實現解析:https://blog.csdn.net/lch_2016/article/details/81045480
相關HashMap相關的面試問題:https://blog.csdn.net/suifeng629/article/details/82179996