學會扒源碼-HashMap

hashmap-node

好久不見,最近我要複習了,隨時準備面試,順便整理筆記,所以這又是一篇沒有感情的純物理輸出!!!

哎 !如果你也準備面試就看看吧。

正文

這一看就是HashMap結構不用說了吧

學會扒系統層源碼

HashMpa源碼分析

這裏,我嘗試拋棄1.8之前都源碼分析,技術在進步,從現在開始分析1.8之後的版本區別。

結構:數組+鏈表 位於java.util包中

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable 

靜態內部類實現了map的接口,允許使用null值,null鍵。

JDK 1.8採用的是Node鏈表

/**
 * Basic hash bin node, used for most entries.  (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

hashmap就是一個entry的數組,entry對象中包含了 key 和 value,其中 next 是爲了解決 hash 衝突構成的一個鏈表。

image-20200603172755101

負載因子:0.75。

static final float DEFAULT_LOAD_FACTOR = 0.75f;

public HashMap() {
     this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

HashMap中的數據量 / HashMap的總容量(initialCapacity),

當loadFactor達到指定值或者0.75時候,HashMap的總容量自動擴展一倍,以此類推。

負載因子代表了hash表中元素的填滿程度。加載因子越大,填滿的元素越多,但是衝突的機會增大了,鏈表越來越長,查詢速度會降低。反之,如果加載因子過小,衝突的機會減小了,但是hash表過於稀疏。衝突越大,查找的成本就越高。

數組大小初始值

    /**
     * The default initial capacity - MUST be a power of two.
     */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

數組Node的初始化值是16,使用位與運算速度更快。

HashMap最大容量

static final int MAXIMUM_CAPACITY = 1 << 30;

必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換)

當看到 1<<30 時,對“<<” 有點模糊,當了解“<<”的用法之後,又有一個問題;

int類型不是4個字節共32位嗎,爲什麼不是 1<<31呢?

首先介紹下等號右邊數字及字符的含義:

1、"<<"爲左移運算符,1表示十進制中的“1”,30表示十進制數字1轉化爲二進制後向左移動30位。在數值上等同於2的30次冪。

2、爲什麼是2的30次冪?

以一個字節爲例:

1<<2 = 4

0000 0001(十進制1)

向左移動兩位之後變成

0000 0100(十進制4)

可見 1<<30 等同於十進制中2的30次冪。

回到題目,爲什麼會是2的30次冪,而不是2的31次冪呢?

首先:JAVA規定了該static final 類型的靜態變量爲int類型,至於爲什麼不是byte、long等類型,原因是由於考慮到HashMap的性能問題而作的折中處理!

由於int類型限制了該變量的長度爲4個字節共32個二進制位,按理說可以向左移動31位即2的31次冪。但是事實上由於二進制數字中最高的一位也就是最左邊的一位是符號位,用來表示正負之分(0爲正,1爲負),所以只能向左移動30位,而不能移動到處在最高位的符號位!

此處參考原文鏈接:https://blog.csdn.net/qq_33666602/article/details/80139620

HashMap中的紅黑樹

由鏈表轉換成樹的閾值TREEIFY_THRESHOLD:8

解釋:一個桶中bin(箱子)的存儲方式由鏈表轉換成樹的閾值。即當桶中bin的數量超過TREEIFY_THRESHOLD時使用樹來代替鏈表。默認值是8。

static final int TREEIFY_THRESHOLD = 8;

由樹轉換成鏈表的閾值UNTREEIFY_THRESHOLD:6。當執行resize操作時,當桶中bin的數量少於UNTREEIFY_THRESHOLD時使用鏈表來代替樹。默認值是6

static final int UNTREEIFY_THRESHOLD = 6;

構造方法

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
      
      
       //這裏的threshold按照定義應該再乘一個加載因子。沒有乘的原因是沒有對table進行初始化,在put裏面會對其進行初始化的。這裏有一個問題,我在初始化加載因子的時候,貌似只能初始化大於1的數字,這個地方留着,有待商榷。
        this.threshold = tableSizeFor(initialCapacity);

第一個參數有兩個的。這裏首先保證了參數和合法性。當參數合法的時候,將輸入的容量轉換爲距離該樹最近的2的整數次冪。調用的tableSizeFor函數,分析如下:可以看到能夠得到距離該數最近的整數次冪了。

    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

HashMap重寫equals方法做了什麼

public final boolean equals(Object o) {
    if (o == this)
        return true;
    if (o instanceof Map.Entry) {
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;
        if (Objects.equals(key, e.getKey()) &&
            Objects.equals(value, e.getValue()))
            return true;
    }
    return false;
}

相比Object方法equals多了一個if判斷,要判斷Entry數據裏的key和value同時相等,因爲hashcode可能key有碰撞,意思就是key相同,value不同,這個時候value在一個鏈表上,需要重寫equals方法,確保value相同。

思考:爲什麼要同時重寫 equals & hashcode?

put方法源碼分析

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

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
      
      
       //判斷了該hash表是否爲空,如果爲空,需要resize
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
      
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
           //else中即爲hash出現了衝突(但是他們的key不一定衝突的,這裏要注意)。
           //這個if的就是key相同而且hash還相同的,對於這種的,就需要直接修改這個節點的值,所以講p賦值於e,對e進行稍後的處理。
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode) //jdk1.8中引入了紅黑樹,如果鏈表的長度超過了8,就會把鏈表轉化爲紅黑樹。這裏就判斷p是否爲紅黑樹的節點。
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {  //這裏的else說明,要麼hashcode的值相同,但是key不同,那麼就要開始遍歷當前的鏈表,一直遍歷到鏈表末尾。如果出現了以下的情況:
                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   //這裏的TREEIFY_THRESHOLD爲8,是當鏈表長度超過8,就會將鏈表便轉化爲紅黑樹。如果在長度以內,便在鏈表末尾new一個節點,把數據存儲。
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    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;
    }

調用的put方法中,可以看到會計算出這個key值的hashcode,然後將該hashcode,key,value作爲參數調用putVal方法。

  1. 首先根據hash(key)&(n-1)取得hash值二進制低m位找到index,這樣的散列算法使key比較均勻的分佈在各個桶裏,找到 index索引到Node節點,如果爲空,直接put在此節點,否則判斷是否是紅黑樹,如果是則找到紅黑樹部分的節點直接put,否則查找鏈表中下一個Node節點的key值和hash等於插入的key和hash值的話直接更新Node節點的value值,否則找到Node節點爲NULL的節點,

  2. 判斷鏈表個數是否超過閾值7,超過鏈表轉換爲紅黑樹,不超過在鏈表new 新出的節點,然後判斷HashMap的節點數是否大於閾值(負載因子*table的長度),大於的話resize擴容,否則不擴容。

image-20200603173316683

get方法源碼分析

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
 * Implements Map.get and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @return the node, or null if none
 */
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; 
   Node<K,V> first, e; 
   int n; 
   K k;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
              // 遍歷紅黑樹,調用equals方法判斷key對應的value
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}
image-20200603173131206

vx搜:【轉行程序員】 拉你進羣

擴容函數resize源碼分析

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table; //定義了一個臨時Node類型的數組
        int oldCap = (oldTab == null) ? 0 : oldTab.length; //判斷目前的table數組是否爲空,記錄下當前數組的大小
        int oldThr = threshold; //記錄下原始的hashmap的大小的閾值
        int newCap, newThr = 0; //新Cap的大小和閾值
        if (oldCap > 0) {//原table不爲空
            if (oldCap >= MAXIMUM_CAPACITY) {//原數組的大小超過2^30
                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) 
            newCap = oldThr; // 用構造器初始化了閾值,將閾值直接賦值給容量
        else { // 原始的閾值和容量值都爲0,使用默認的閾值和容量值進行初始化,這個在我們new HashMap就是這麼處理的。
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {//這裏爲0,說明進入了else if或者if,即爲原table不爲空,或者初始化的時候用構造器人爲設定的閾值
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//初始化新的Node類型數組
        table = newTab;
        //當原來的table不爲空,需要將數據遷移到新的數組裏面去
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {//開始對原table進行遍歷
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {//取出這個數組第一個不爲空的元素
                    oldTab[j] = null;//把空間釋放掉
                    if (e.next == null)//說明這個entry對應的鏈表中只有一個節點,
                        newTab[e.hash & (newCap - 1)] = e;計算相應的hash值,把節點存儲到新table的對應位置處
                    else if (e instanceof TreeNode)//若e是紅黑樹的節點,要按照紅黑樹的節點移動
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order,說明此鏈表中不止一個節點,需要全部移到新的table中
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        //這裏的循環是將鏈表按照(e.hash & oldCap) == 0,把節點分成兩部分,進行了一次rehash lo串的位置與原來相同,hi串的位置爲原來位置+oldCap。
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

對於rehash這一段,還是有必要講解一下的。剛開始我是一點都不懂這是幹什麼玩意呢,後來理解了一下。

因爲我們的閾值和容量都會變爲原來的2倍。想想一下,如果hash值和原大小相與爲0,說明啥?說明了即使閾值和容量變爲原來的2倍,索引還是不會變。比較難懂,舉個例子:

cap:0000 1111
key1:0000 0000
key2;0000 0101

key1&cap = 0;key2&cap != 0

當我cap變爲原來的2倍時,

cap:0001 1111

key1&cap:0000 0000

key2&cap:0001 0101

發現擴容後的key1的索引不變,key2的變了。那麼有人會問,爲毛key1&cap這個就是索引?其實這裏的key是索引,即key.hashcode,那麼我這個hashcode&cap=hashcode,這個是沒錯的。所以纔有了以上的種種推斷。這個是設計思路,而上面是代碼實現。這個設計很巧妙,不用重新計算hashcode,省去了不少時間。

Node鏈表尾部插入法原因(1.8之前是頭部插入法)

併發put導致環形結構,get操作時⽆無限循環。

image-20200603173833618
image-20200603173937989
image-20200603174008821
image-20200603174152896

結果就是造成一個環。

聯繫我

VX搜索【轉行程序員】回覆”加羣“,我會拉你進技術羣。講真的,在這個羣,哪怕您不說話,光看聊天記錄也是一種成長。**阿里/騰訊/百度資深工程師、Google技術大神、IBM工程師、還有我王炸、各路大牛都在,有任何不明白的都進羣提問。

最後,覺得王炸的文章不錯就來個三連吧:關注 轉發 點贊

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