【源碼分析】HashMap(一)

花了幾個小時的時間,給本身總結一下所學的

java

1、HashMap

HashMap 是基於哈希表 Map 接口的一個實現,經過 Key-Value (key、value都支持 null)存放數據,在 JDK1.7 時,底層是 數組 + 鏈表JDK1.8 改成了 數組 + 鏈表 + 紅黑樹
HashMap 實現了 Map 接口, HashMap 中的 Node 靜態內部類則是實現了 Map 接口中的內部接口 Entrynode

ha.jpg

靜態內部類 Node 是一個單向鏈表, 對應HashMap的拉鍊式存儲. 它實現了 getKey、getValue、setValue、hashCode、equals 這些函數數組


2、數組 + 鏈表 + 紅黑樹

HashMap 爲何採用這種實現方式app

11111.jpg

1. 容量、加載因子

首先介紹 HashMap 中兩個概念: 容量加載因子
HashMap 的默認初始容量爲 16 ,加載因子爲 0.75f
實際容量 = 16 * 0.75 = 12less

加載因子指的是元素對容器的充滿程度,當元素達到這個充滿程度就會進行自動擴容
爲何默認規定負載因子是 0.75,而不是0.8,0.76 函數

由於負載因子越大,表示可填充的程度越大,那麼空間利用率越大,但鏈表的的長度就會愈來愈大,查詢的效率就會下降,同時hash衝突的機會也會增長
負載因子越小,表示可填充的程度越小,那麼空間的利用綠越小,形成空間資源浪費,可是鏈表的長度短,hash衝突的機會小,查詢效率高大數據

因此 0.75f 是官方給出的一種時間和空間權衡的 折衷選擇this

transient Node<K,V>[] table; // 存放內容的實體數組
 transient int size;    // 存放的大小
 transient int modCount;    // 被修改的次數
 int threshold;   // 臨界值 = 容量 * 加載因子
 final float loadFactor;    // 加載因子

transient 關鍵字 表示不參與序列化過程spa


2. hash衝突

hashmap存放鍵值對時,經過對象的hashCode 算出 hash 值來肯定存儲位置的,當hashCode同樣時,hash值就是同樣的, 當存儲的對象多時,可能會出現不一樣的對象 hash 值相同,這就是hash 衝突,hashmap 底層是經過鏈表來解決hash衝突的.3d


3. 爲何轉換成紅黑樹結構

hashmap默認初始化是鏈表存儲的,當鏈表的長度過長時,查詢效率慢,在相同條件下 鏈表的查詢複雜度爲O(n),樹型的查詢複雜的爲O(log(n)). 將查詢效率從 O(n) 提高到 O(log(n))

何時才轉換

首先,看代碼

/*
     * 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
     */

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    static final int UNTREEIFY_THRESHOLD = 6;

定義在 hashmap中的 TREEIFY_THRESHOLD = 8, 8 鏈表轉成樹的閾值
當hashmap 中一個鏈表的節點足夠多時(由於TreeNodes佔用空間是普通Nodes的兩倍),長度達到了 8 ,就轉換成紅黑樹
當紅黑樹的節點長度降到爲 6 時,又轉成鏈表

爲何閾值設置爲 8 而不是其餘值
在 hashCode 離散性均勻的狀況下,hashmap 中數據的位置均勻,很小的機率會用到紅黑樹結構,幾乎鏈表的長度不會達到 8
然而隨機 hashCode 離散性不是很好的狀況下,JDK 又不能阻止用戶實現離散性低的hashCode,所以就可能致使不均勻的數據分佈

根據什麼大數據機率統計,泊松分佈,得出
hashCode理想狀況下鏈表的長度能達到 8 時的機率爲 0.00000006 ,幾乎是不發生事件,因此閾值爲 8

3、進入putVal方法

執行語句 map.put("lankeren","1069941886") 時,調用的時是

public V put(K key, V value) {
       return putVal(hash(key), key, value, false, true);
     }

見招拆招,進入 putVal()
先經過 對象hashCode 算出 hash 值

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

用到了 ^ 運算

/**
     *  map.put()  
     **/
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
                       
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0) 
            // 若是table數組還沒初始化的進行初始化操做
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)  // 獲取該 bin 的頭節點
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 若插入的key已經存在哈希映射了(mapping)
               // 將當前節點賦值給 e  而後進行新舊值更新
                e = p;
                
            else if (p instanceof TreeNode)
                // 若是當前節點屬於樹結構的,建立樹結構型節點存放
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 頭節點沒有發生重複,也不是樹型結構
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        // 若是發生已存在該key的哈希映射(mapping)
                        break;
                        // break,而後對該key進行新舊值更新
                        
                    p = e;
                }
            }
            
           if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
           }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        // 暫時不知道做用
        afterNodeInsertion(evict); 
        return null;
    }


思路:

將數據加入到哈希表中時,
1. 先對實體數組進行初始化(默認長度16) 
2. 判斷該 hash 值對應的位置(table數組下標)是否已經有哈希映射了
    1. 若是有,進入 else 
    2. 若是沒有,存進該頭部
3. 當前鏈表不爲空,判斷待插入的key是否已存在哈希映射
    1. 與頭節點判斷
    2. 與頭節點以後節點的判斷
    3. 是否屬於樹型結構的節點
4. 更新新舊值
5. 增長被修改次數
6. 是否大於臨界值
    1. 是 --> 擴容
    2. 不是  --> 無操做


dn.jpg


返回: 若是首次插入該數據位置,返回null,若是新舊value更換,返回舊的value

相關文章
相關標籤/搜索