最通俗易懂的HashMap原理詳解(JDK 1.8)

一、HashMap的數據結構

 HashMap是存儲鍵值對的集合,當然HashMap也是線程不安全的,每一個鍵值對存儲在一個Node<K,V>(JDK1.7中是Entry<K,V>。HashMap的主幹是一個名爲table的Node數組。

 每個鍵值對Key的hash值就對應數組的下標,當遇到hash衝突時,HashMap採用的策略是鏈地址法。在JDK1.7中通過鍵值對Entry<K,V>中的next屬性來把hash衝突的所有Entry連接起來,因此每次都要遍歷鏈表才能得到所要找的鍵值對,增刪改查操作的時間複雜度爲O(n)。而在JDK1.8中,當鏈表長度大於8時,會將鏈表轉化爲一棵紅黑樹,增刪改查操作的時間複雜度爲O(log(n))。

 注意,HashMap運行Key爲null,並且存儲在table[0]的位置,table[0]的位置只能存儲一個Key爲null的鍵值對。(Hashtable不允許key爲null)。

 

二、源碼分析

1、Node<K,V>鍵值對。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//hash值
        final K key;//鍵值
        V value;
        Node<K,V> next;
        ……
}
 key和hash屬性爲final的原因:在Java中,如果一個對象的屬性值在業務邏輯上不需要改變,就將其聲明爲final,這樣保證了安全性。而且在這裏若是讓key或者hash發生改變,會導致該鍵值對無法被查找到。

2、重要屬性

transient int size;//實際鍵值對個數
int threshold;//閾值,超過到這個值後就要進行擴容
transient int modCount;//修改計數器,用於快速失敗
final float loadFactor;//加載因子
 注意threshold = table.length (默認值爲16)* loadFactor(默認值爲0.75)這兩個值可以在構造時自行輸入。length值需要自己根據業務需求輸入,輸入合適的值能顯著減少擴容次數 。而loadFactor值在一般情況下0.75都是有不錯的效率的。但是若是對時間效率要求很高,對空間效率要求很低,可以減小loadFactor值,反之增大loadFactor值,可以大於1。modCount用於記錄對象的改變結構的次數(不包含修改value的值的操作),這是用於在多線程情況下,當多個線程併發修改HashMap的結構時,多個線程都會去修改modCount這個成員變量,而每個線程內部維護着一個局部變量的修改技術器,當線程做完修改操作後發現成員變量的modCount與局部變量不一致時,就拋出ConcurrentModificationException,這是fail-fast機制,Java中如ArrayList等線程不安全的集合都有這個機制。

3、如何求key的hash值

 在HashMap中,計算key的hash值,也就是在table數組中的下標位置算法很巧妙,用到了許多位運算。
主要分爲三步:
 (1)計算對象自己的hashCode()值
 (2)計算上步得到的值高16位於低16位相與。否則在容器length較小時,無法發揮高位的作用,這樣能使 得hash分佈更加均勻,減少衝突。
 (3)定位操作,對上步計算得到hash值進行取模操作(這裏用的是位運算,有個小技巧)。

第一步與第二步:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//第一步與第二步
    }
 第三步是取模定位,在JDK1.7中是一個indexFor函數,而JDK1.8取消了這個函數,但是JDK吧把這個index操作整合到put操作和get操作中去了,當然其代碼原理都是一樣的。
核心代碼:
index = (n - 1) & hash//在這裏等價於hash%n
 這裏你一定想問,爲什麼這個位運算能夠與取模運算相等了,這裏就要歸功於HashMap對容量的一個巧妙設計。HashMap規定length一定是2^N,N可爲任意整數,如果自定義的length長度不爲2的整數次冪,那麼就會自動取成大於設定值的最接近的2^N的值。在這個前提下,我們再來看這個二進制與運算。如在n=16的情況下,15的二進制碼爲 1111,不管是什麼數,和11111做&操作,低五位不會變,而高位全部爲0,即與hash%n的結果是一樣的。而且在計算機中,位運算的效率是最高的,因此這樣會大大提升查詢效率。

4、擴容操作(resize())

需要擴容的情況:
if (++size > threshold)//當完成put操作後,發現新的size大於了閾值
            resize();
每次擴容爲之前的兩倍:
newThr = oldThr << 1; // double threshold
在擴容之後就要把具體鍵值對搬遷到新的table數組中。


5、put方法具體流程

(圖片來自網絡)




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章