分析分析HashMap

1.HashMap 數據結構

數組鏈表
數組的結構大概是這樣的
在這裏插入圖片描述
當我們put<k,v>值進去的時候 HashMap會根據key進行一個hash算法去計算一個值與數組長度(n-1)做與&運算 算出index ,這個index類似數組地址位置。

//進行hash算法
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
 //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)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        		// i =(n - 1) & hash  算出key的下標位置
            tab[i] = newNode(hash, key, value, null);  
            

由於hash 2個不同的key 有概率會導致hash值一樣
例如put(k1,v1)和put(k2,v2) 有可能出現hash(k1) ==hash(k2)
這時候就需要用到鏈表的結構了。這個時候put(k2,v2) 就會加入到鏈表當中。
在這裏插入圖片描述
鏈表中的每個節點 Node <k,v> 都會有hash key 和value 以及指向的下個節點

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;//指向的節點
        ....
        }

2 頭插法 和尾插法

在java8之前 新的加入的節點採用的是頭插法 就是說新加入的節點會取代原來的值,其他值順着鏈表往後移動。
原因 :作者認爲 新插入的值被查找的機率更大。這樣可以提升查找效率。
但是在java8之後採用的是尾插法
主要原因 :頭插法有個弊端 在多線程插入的時候由於當數組進行擴容的時候resize() 多個線程put 時候可能會出現環形鏈表如果這個時候去取值,悲劇就出現了——Infinite Loop。

Java7在多線程操作HashMap時可能引起死循環,原因是擴容轉移後前後鏈表順序倒置,在轉移過程中修改了原來鏈表中節點的引用關係。
Java8在同樣的前提下並不會引起死循環,原因是擴容轉移後前後鏈表順序不變,保持之前節點的引用關係

在這裏插入圖片描述

3.擴容

擴容主要是負載因子LoadFactor當前hashmap長度Capacity 有關係 就是 負載因子 * hashmap容量 < Capacity 就會進行擴容resize()

1.創建一個新的數組 這個數組的大小是原來的長度的2倍。
2.重新 hash原來的數組內容   把舊的數組重新加到新數組
重新進行hash算法 是因爲在上面有提到  key的位置不僅和hash()這個方法有關還和數組長度有關所有需要重新計算位置。
 final Node<K,V>[] resize() {
        //舊的數組
        Node<K,V>[] oldTab = table;
        //數組長度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //要進行擴容的值
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            //如果大於hashmap容量最大值則不擴容了
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //擴容 數組擴大一倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //
        threshold = newThr;
     
                //新建立一個數組
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        //重新計算下標位置
                        newTab[e.hash & (newCap - 1)] = e;
                        ....省略很多代碼
                        }

同時看源碼可以看到定義當鏈表的 長度大於8的時候 會將鏈表轉化爲紅黑樹。
當鏈表的長度小於6的時候 會將紅黑樹轉化爲鏈表
數組默認初始容量是16(2的n次方)這樣是爲了位運算的方便 默認容量DEFAULT_INITIAL_CAPACITY = 2的n次方時候 DEFAULT_INITIAL_CAPACITY -1 的二進制都是1 這樣我們前面說的 index的結果等同於HashCode後幾位的值。
只要輸入的HashCode本身分佈均勻,Hash算法的結果就是均勻的

 /**
     * 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;
    
    /**
     * The default initial capacity - MUST be a power of two. 默認初始化容量16 必須是2的次方
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

接着添加操作講解。添加操作的執行流程爲:

先判斷有沒有初始化
再判斷傳入的key 是否爲空,爲空保存在table[o] 位置
key 不爲空就對key 進hash,hash 的結果再& 數組的長度就得到存儲的位置
如果存儲位置爲空則創建節點,不爲空就說明存在衝突
解決衝突HashMap 會先遍歷鏈表,如果有相同的value 就更新舊值,否則構建節點添加到鏈表頭
添加還要先判斷存儲的節點數量是否達到閾值,到達閾值要進行擴容
擴容擴2倍,是新建數組所以要先轉移節點,轉移時都重新計算存儲位置,可能保持不變可能爲舊容量+位置。
擴容結束後新插入的元素也得再hash 一遍才能插入。
獲取節點的操作和添加差不多,也是

先判斷是否爲空,爲空就在table[0] 去找值
不爲空也是先hash,&數組長度計算下標位置
再遍歷找相同的key 返回值

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