HashMap原始碼個人解讀

=凌晨=發表於2021-04-02

HashMap的原始碼比較複雜,最近也是結合視訊以及其餘大佬的部落格,想著記錄一下自己的理解或者當作筆記

JDK1.8後,HashMap底層是陣列+連結串列+紅黑樹。在這之前都是陣列+連結串列,而改變的原因也就是如果連結串列過長,查詢的效率就會降低,因此引入了紅黑樹。

HashMap原始碼個人解讀

 

這裡的連結串列是一個單向連結串列

 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;
        }
    }

接下來是類的屬性

 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16預設的初始容量是16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;//負載因子

    /**
     * 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 smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;//桶中的結構轉化為紅黑樹對應的table,也就是桶的最小數量。

transient Node<K,V>[] table;//存放元素的陣列,總是2的冪次方

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;存放具體元素的集

    /**
     * The number of key-value mappings contained in this map.
     */
    transient int size;存放元素的個數,不是陣列的長度

    /**
     * The number of times this HashMap has been structurally modified
     * Structural modifications are those that change the number of mappings in
     * the HashMap or otherwise modify its internal structure (e.g.,
     * rehash).  This field is used to make iterators on Collection-views of
     * the HashMap fail-fast.  (See ConcurrentModificationException).
     */
    transient int modCount;//每次擴容和更改map結構的計數器

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     * @serial
     */
    // (The javadoc description is true upon serialization.
    // Additionally, if the table array has not been allocated, this
    // field holds the initial array capacity, or zero signifying
    // DEFAULT_INITIAL_CAPACITY.)
    int threshold;//臨界值,當實際大小(容量*負載因子)超過臨界值時,會進行擴容

    /**
     * The load factor for the hash table.
     *
     * @serial
     */
    final float loadFactor;//負載因子

構造方法中將兩個引數的構造方法

 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)//初始容量不能小於0,否則報錯
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY)//初始容量不能大於最大值,否則為最大值 initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor))//負載因子不能小於或者等於0,不能為非數字 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor;//初始化填充因子 this.threshold = tableSizeFor(initialCapacity);//初始化threshold大小 }
tableSizeFor(initialCapacity)這個方法的作用就是返回大於等於initialCapacity的最小的二的次方數。注意是最小
    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;//0b1001
        n |= n >>> 1;//1001 | 0100 = 1101
        n |= n >>> 2;//1101 | 0011 = 1111
        n |= n >>> 4;//1111 | 0000 = 1111
        n |= n >>> 8;
        n |= n >>> 16;//那麼後面這兩步就得到的結果還是1111。
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;//1111就是15.加1等於16
    }

加色cap等於10,那麼n = 10-1 = 9。n轉化為二進位制的話,就是0b1001。那麼無符號右移一位,就是0100。

這裡cap-1的操作就是為了保證最後得到的n是最小的大於等於initialCapacity的二的次方數。比如這裡比10大的2的次方數就是16。如果沒有減1.經過上述多次右移和或運算之後,得到的就不是16了。而是32。就不是最小的了。就變成了2倍了。

接下來分析put方法。

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

首先研究hash(key)這個方法

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

這個就叫擾動函式,他讓hash值得高16位與第16位進行異或處理。這樣可以減少碰撞。採用位運算也是因為這樣更高效。並且當陣列的長度很短時,只有低位數的hashcode值能參與運算。而讓高16位參與運算可以更好的均勻雜湊,減少碰撞,進一步降低hash衝突的機率。並且使得高16位和低16位的資訊都被保留了。

然後講述putVal方法,執行過程可以用下面圖來理解:

HashMap原始碼個人解讀

1.判斷陣列table是否為空或者位null,否則執行resize()進行擴容;

2.根據鍵值key計算陣列hash值得到插入的陣列索引i,如果table[i]==null,那麼就可以直接新建節點新增到該處。轉向6,如果不為空,轉向3

3.判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向4,這裡的相同指的是hashCode以及equals;
4.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向5;
5.遍歷table[i],判斷連結串列長度是否大於8(且),大於8的話(且Node陣列的數量大於64)把連結串列轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行連結串列的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;
6.插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。
————————————————
版權宣告:本文為CSDN博主「錢多多_qdd」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。
原文連結:https://blog.csdn.net/moneywenxue/article/details/110457302

原始碼如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;//定義了輔助變數tab:引用當前hashMap的雜湊表;p:表示當前雜湊表的元素,n:表示雜湊表陣列的長度 i:表示路由定址結果
      //這裡是延遲初始化邏輯,第一次呼叫putVal時會初始化hashMap物件中的最耗記憶體的雜湊表
//    步驟1
if ((tab = table) == null || (n = tab.length) == 0)//table就是Hash,table就是HashMap的一個陣列,型別是Node[],這裡說明雜湊表還沒建立出來 n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null)//n在上面已經賦值了。這是步驟2。 tab[i = (n - 1) & hash])這一塊就是路由演算法,賦予p所在table陣列的位置,並把這個位置的物件,賦給p,如果這個位置的節點位null,那麼表示這個位置還沒存放元素。 tab[i] = newNode(hash, key, value, null);就在該位置建立一個新節點,這個新節點封裝了key value else {//桶中這個位置有元素了 Node<K,V> e; K k;//步驟3 if (p.hash == hash &&//如果當前索引位置對應的元素和準備新增的key的hash值一樣 ((k = p.key) == key || (key != null && key.equals(k))))並且滿足準備加入的key和該位置的key是同一個物件,那麼後續就會進行替換操作。 e = p; else if (p instanceof TreeNode)//步驟4.判斷該鏈是否是紅黑樹 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);如果是,則放入該樹中 else {//步驟5 該鏈為連結串列 使用尾插法插入資料 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) {//桶的位置的next為e,如果e為null,在for的迴圈中就說明沒有找到一樣的key的位置,那麼久加入末尾 p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st加入後判斷是否會樹化 treeifyBin(tab, hash); break;//然後跳出 } if (e.hash == hash &&//這種情況就是找到了一個key以及hash都一樣了,那麼久要進行替換。 ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e;//這是迴圈用的,與前面的e=p.next組合,可以遍歷連結串列。 } } if (e != null) { // existing mapping for key//這裡就是替換操作,表示在桶中找到key值,hash值與插入元素相等的節點 V oldValue = e.value;//記錄e的value if (!onlyIfAbsent || oldValue == null) e.value = value;//用心智替換舊值, afterNodeAccess(e);//訪問後回撥 return oldValue;//返回舊值 } } ++modCount;//結構性修改 if (++size > threshold)//步驟6,如果超過最大容量就擴容。 resize(); afterNodeInsertion(evict);//插入後回撥 return null; }

 總結一下流程:1根據key計算得到key.hash = (h = k.hashCode())^(h>>>16);

2.根據key.hash計算得到桶陣列中的索引,其路由演算法就是index = key.hash &(table.length-1),就是雜湊值與桶的長度-1做與操作,這樣就可以找到該key的位置

2.1如果該位置沒有資料,那正好,直接生成新節點存入該資料

2.2如果該位置有資料,且是一個紅黑樹,那麼執行相應的插入/更新操作;

2.3如果該位置有資料,且是一個連結串列,如果該連結串列有這個資料,那麼就找到這個點並且更新這個資料。如果沒有,則採用尾插法插入連結串列中。

接下來講解最重要的resize()方法。

擴容的目的就是為了解決雜湊衝突導致的鏈化影響查詢效率的問題。擴容可以緩解。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//oldTab引用擴容前的雜湊表
     ////oldCap表示擴容之前table陣列的長度
      //oldTab==null就是第一次new HashMap()的時候,那時候還沒有放值,陣列就是null。那麼初始化的時候
      //也要擴容。這句就是如果舊的容量為null的話,那麼oldCap是0,否則就是oldTab的長度
int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold;//表示擴容之前的擴容閾值,也就是觸發本次擴容的閾值
    //newCap:擴容之後table陣列的大小
//newThr:擴容之後,下次再次觸發擴容的條件
int newCap, newThr = 0; if (oldCap > 0) {//條件如果成立,那麼就是代表hashMap中的雜湊表已經初始化過了,這是一次正常的擴容 if (oldCap >= MAXIMUM_CAPACITY) {//擴容之前的table陣列大小已經達到最大閾值後,則不擴容,且設定擴容條件為int最大值,這種情況非常少數 threshold = Integer.MAX_VALUE; return oldTab; }
        //oldCap左移一位實現數值翻倍,並且賦值給newCapnewCap小於陣列最大值限制 且 擴容之前的閾值>=16
* //這種情況下,則下一位擴容的閾值等於當前閾值翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold這裡閾值翻倍也可以理解,如果原先table是16長度,那麼oldThr就是16*0.75=12;那麼oldCap翻倍的時候,那麼新的閾值就是2*16*0.75 = 24; }
     //這是oldCap==0的第一種情況,說明hashMap中的雜湊表是null
     //哪些情況下雜湊表為null,但是閾值卻大於零呢
    //1.new HashMap(initCap,loadFactor);
    //2.new HashMap(initial);
    //3.new HashMap(map);並且這個map有資料,著三種情況下oldThr是有值得
else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { 這是oldCap==0,oldThr==0的情況,是new HashMap();的時候// zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY;//16 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12 } if (newThr == 0) {//newThr為0時,通過newCap和loadFactor計算出一個newThr 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];//建立一個大小為newCap的新的陣列 table = newTab;//把新的陣列賦值給table if (oldTab != null) {//說明hashmap本次擴容之前,table不為null for (int j = 0; j < oldCap; ++j) {//將舊陣列中的所有資料都要處理,所以來個迴圈 Node<K,V> e;//當前node節點 if ((e = oldTab[j]) != null) {//說明當前桶位中有資料,但是資料具體是單個資料,還是連結串列還是紅黑樹並不知道 oldTab[j] = null;//將舊的陣列的這個點置空,用於方便VM GC時回收記憶體 if (e.next == null)//如果當前的下一個不為空,也就是在桶位中是單個資料, newTab[e.hash & (newCap - 1)] = e;//那麼根據路由演算法e的hash與上新的table的長度-1,得到索引,然後該索引放入e這個單個資料 else if (e instanceof TreeNode)//第二種情況,當前節點已經樹化。 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order//第三種情況,連結串列的時候,圖可看下,
               //低位連結串列:存放在擴容之後的陣列的下標位置,與當前陣列的下標位置一致
Node<K,V> loHead = null, loTail = null;
              //高位連結串列:存放在擴容之後的陣列的下標位置為當前陣列下標位置+擴容之前陣列的長度。可見下方解釋。 Node
<K,V> hiHead = null, hiTail = null; Node<K,V> next; do {//這裡是判斷節點應該在高鏈還是低鏈。 next = e.next;
                //假如oldCap = 16,那麼就是0b10000
                //假如hash為 .....1 1111.前面的不用看,就看著五位
                //或者hash為 .....0 1111.
                那麼與上 000000.. 1 0000.前面都是0。所以與完之後,如果為0,那麼就是下面這種.....0 1111,就代表應該在低位鏈。
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) {//如果原先舊陣列中的連結串列中低位鏈後面不為null,也就是後面是高位鏈的。複製到新的陣列中就要置為null。 loTail.next = null; newTab[j] = loHead;//然後把這個低連結串列的頭節點放到新的陣列中的索引位置。這樣低位鏈的這個節點,就到了新的陣列的地方了 } if (hiTail != null) {//同理 hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }

從下圖可以看出上面擴容的第三種情況,連結串列的情況。在第15這個桶位的時候,為啥擴容到32的長度的時候,有的連結串列節點還在15,而有一些卻到31處了。因為舊陣列中的索引15是根據路由演算法算出來的,

公式為hash&(table.length-1),此時索引為15,那麼就是hash&(table.length-1) =15.又因為table.length-1=15,也就是1111,那麼hash的低四位肯定就知道了,也是1111,但是第五位就不知道了有可能是1有可能

是0,也就是11111或者01111.那麼在新的陣列中求索引的時候,根據路由演算法,此時新的陣列長度為32,那麼32-1=31,也就是11111那麼舊陣列中的數如果是11111,那麼算出來就是11111,就是在新陣列

31的位置上,如果是01111,那麼與之後就是01111。就還是15。別的位置也是這樣,如果是原索引為1的地方,那麼有可能到新陣列的17的位置,就是1+16=17;不是都是加16;而是加這個舊陣列的長度。

 

接下來是get()方法;

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;//這裡hash(key)的原因是因為存的時候,hash了一下,那麼取的時候,肯定的要取他的hash值一樣的。
    }

所以主要是getNode方法:

final Node<K,V> getNode(int hash, Object key) {
    ////tab:引用當前hashMap的雜湊表
    //first:桶位中的頭元素
    //e:臨時node元素
     //ntable陣列長度
Node
<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && //(tab = table) != null&& (n = tab.length) > 0表示雜湊表不為空,這樣才能取到值,要不然沒有水的水池不可能取到水。 (first = tab[(n - 1) & hash]) != null) {//這個代表在這個索引的位置的頭節點不為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) {//如果這個桶的索引處不是單個資料,那麼就進一步查詢,如果是單個元素,那麼下面不執行,直接返回null,因為沒有找到 if (first instanceof TreeNode)//如果是樹,那麼就去找 return ((TreeNode<K,V>)first).getTreeNode(hash, key); do {//桶位這裡所在的節點是連結串列 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e;//如果找到一樣的e,那麼就返回,否則就一直迴圈,到結尾的話,就還沒找到,那麼就返回null } while ((e = e.next) != null); } } return null;//如果上述條件不滿足,也就是桶為null或者在指定位置沒有資料,那麼就返回null }

接下來是remove(Object key)方法。

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

其實就是removeNode方法

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
    //matchValue是用來判斷的,因為remove還有一個方法,是兩個引數的,remove(Object key, Object value).這個方法也是套娃了removeNode方法,意思是不僅key得一致,value也得一致才能刪除。否則刪不了。matchValue就是用來做這個判斷得
* //tab:引用當前hashMap中的雜湊表
* //p:當前node元素
* //n:表示雜湊表陣列長度
* //index:表示定址結果,索引位置。
Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 &&//這裡還是一樣,先判斷有沒有水,不然不需要取水了。 (p = tab[index = (n - 1) & hash]) != null) {//找到對應的桶位是有資料的,要不然為null還刪啥 Node<K,V> node = null, e; K k; V v; //node為查詢到的結果, e表示當前node的下一個元素 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))//當前定位的桶位的索引處的頭節點就是要找的結果 node = p;//那麼把p賦值給node else if ((e = p.next) != null) {說明當前桶位的頭節點有下一個節點,要麼是紅黑樹,要麼是連結串列 if (p instanceof TreeNode)//如果是紅黑樹 node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e;//這裡條件是說明如果是連結串列中的某一個,那麼就找到了這個節點,並把則會個結果賦值給node。用於返回。並退出迴圈 break; } p = e;//這裡是還沒找到繼續挨個迭代 } while ((e = e.next) != null); } }//上述是查詢過程。下面是刪除過程 if (node != null && (!matchValue || (v = node.value) == value ||//!matchValue || (v = node.value) == value就是用來判斷值是否需要判斷一樣再刪除,就是兩個引數的remove方法的條件。如果不是,那麼!matchValue就是對的,後面值得判斷就不用判斷了 (value != null && value.equals(v)))) {//這裡就是判斷是否是要刪除這個節點。 if (node instanceof TreeNode)//這種情況代表結果是樹的節點。就走樹的刪除邏輯 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p)//噹噹前桶位的元素就是要刪除的元素,那麼node才會等於p,那麼就把node之後的頭節點放到這個桶位就行 tab[index] = node.next; else p.next = node.next;//連結串列的情況的時候,node一定是在後面。因為上述查詢過程中e一直都是p.next,e又賦值給node。所以node就在p後面。這裡刪除node節點就行。 ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }

 replace方法主要是呼叫getnode。上文已經講述過了。這裡不贅述。

注意:連結串列轉化為紅黑樹的條件是當前桶位中的節點數到達8並且雜湊表的長度大於等於64。

還有table擴容的時候,並不是只是桶位的第一個元素才算。根據新增函式中的size++;size是隻要加入一個元素,就加1.也就是加入的元素不是桶位第一個元素,而是加到紅黑樹或者連結串列中了。也算。這樣只要達到了閾值0.75*長度。雜湊表table就會擴容。

相關文章