使用HashMap時 注意事項

內部實現
JDK1.8以後 HashMap的數據結構發生了一些改變,從單純的數組加鏈表結構變成數組+鏈表+紅黑樹.如圖
在這裏插入圖片描述
參考博主原創文章, 作者:maohoo https://blog.csdn.net/maohoo/article/details/81506657
的 實踐 : 在此鳴謝
Ctrl+shift+AIt+N 在 彈出框 輸入 HashMap : 按 鍵Enter 呈現源碼 :
ctrl+F : 輸入 static class
在這裏插入圖片描述
在這裏插入圖片描述
其中的Node是HashMap的靜態內部類,實現了Map.Entry接口,本質就是一個KV映射,上圖中的小圓圈就是一個Node

static 修飾的的class 成員變量 與 成員方法 都是 final 的

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;  //  用來定位數組索引位置
    final K key;
    V value;
    Node<K,V> next;// 鏈表的下一個node

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

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;
// tab爲空則創建
if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;
// 計算index,並對null做處理
if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
else {
    Node<K,V> e; K k;
    // 節點key存在,直接覆蓋原來的value
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        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);
                //鏈表長度大於8轉換爲紅黑樹進行處理
                if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    treeifyBin(tab, hash);
                break;
            }
            // key已經存在直接覆蓋原來的value
            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;// modeCount字段主要用來記錄HashMap內部結構發生變化的次數,主要用於迭代的快速失敗
if (++size > threshold)// size是指HashMap中實際存在的鍵值對數量;threshold是指允許的最大元素數目,超過這個數量,需要擴容(resize)
    resize();
afterNodeInsertion(evict);
return null;

}

當JVM存儲HashMap的K-V時,僅僅通過Key來決定每一個Entry的存儲槽位(Node[]中的index).並且Value以鏈表的形式掛載到對應槽位上即可(1.8之後如果長度大於8則轉爲紅黑樹).
HashMap之所以稱之爲HashMap是因爲HashMap在put(String,Object)的時候JVM會對存入的對象進行一次hash(所有對象都是繼承Object,而hashcode方法來自Object類中),從而獲取到這個對象的hash值,接着JVM就根據這個hash值來決定該元素的存儲位置.
如果發生兩個Key存儲到了同一個位置,則發生了Hash衝突(碰撞),Java採用的數組 + 鏈表方式就發揮作用了.Java採用鏈地址法(哈希值相同的元素構成一個鏈表,鏈表頭指針指向Node[]的index),避免了Hash衝突的問題(參考上面的HashMap的圖).Hash衝突發生後,這個槽位中存儲的不是一個Entry而是多個Entry,此時就使用到了Entry鏈表(參見HashMap數據結構).JVM是按照順序去遍歷每一個Entry,一直到查找到對應的Entry爲止(鏈表查詢)

HashMap的擴容機制
對應的resize()源碼如下:

 final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; //當前所有元素所在的數組,稱爲老的元素數組
int oldCap = (oldTab == null) ? 0 : oldTab.length; //老的元素數組長度
int oldThr = threshold; // 老的擴容閥值設置
int newCap, newThr = 0; // 新數組的容量,新數組的擴容閥值都初始化爲0
if (oldCap > 0) {   // 如果老數組長度大於0,說明已經存在元素
    // PS1
    if (oldCap >= MAXIMUM_CAPACITY) { // 如果數組元素個數大於等於限定的最大容量(2的30次方)
        // 擴容閥值設置爲int最大值(2的31次方 -1 ),因爲oldCap再乘2就溢出了。
        threshold = Integer.MAX_VALUE;  
        return oldTab;  // 返回老的元素數組
    }

   /*
    * 如果數組元素個數在正常範圍內,那麼新的數組容量爲老的數組容量的2倍(左移1位相當於乘以2)
    * 如果擴容之後的新容量小於最大容量  並且  老的數組容量大於等於默認初始化容量(16),那麼新數組的擴容閥值
        要麼已經經歷過了至少一次擴容)
    */
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr << 1; // double threshold
}

// PS2
// 運行到這個else if  說明老數組沒有任何元素
// 如果老數組的擴容閥值大於0,那麼設置新數組的容量爲該閥值
// 這一步也就意味着構造該map的時候,指定了初始化容量。
else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
else {               // zero initial threshold signifies using defaults
    // 能運行到這裏的話,說明是調用無參構造函數創建的該map,並且第一次添加元素
    newCap = DEFAULT_INITIAL_CAPACITY;  // 設置新數組容量 爲 16
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 設置新數組擴容閥值爲 16*0.75 = 12。0.75爲負載因子(當元素個數達到容量了4分之3,那麼擴容)
}

// 如果擴容閥值爲0 (PS2的情況)
if (newThr == 0) {
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
              (int)ft : Integer.MAX_VALUE);  // 參見:PS2
}
threshold = newThr; // 設置map的擴容閥值爲 新的閥值
@SuppressWarnings({"rawtypes","unchecked"})
    // 創建新的數組(對於第一次添加元素,那麼這個數組就是第一個數組;對於存在oldTab的時候,
        那麼這個數組就是要需要擴容到的新數組)
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab; // 將該map的table屬性指向到該新數組
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) // 如果元素沒有有下一個節點,說明該元素不存在hash衝突
                // PS3
                // 把元素存儲到新的數組中,存儲到數組的哪個位置需要根據hash值和數組長度來進行取模
                // 【hash值  %   數組長度】   =    【  hash值   & (數組長度-1)】
                //  這種與運算求模的方式要求  數組長度必須是2的N次方,但是可以通過構造函數隨意指定初始化容量呀,
                  如果指定了17,15這種,豈不是出問題了就?沒關係,最終會通過tableSizeFor方法將用戶指定的轉化爲大
                  於其並且最相近的2的N次方。 15 -> 16、17-> 32
                newTab[e.hash & (newCap - 1)] = e;

                // 如果該元素有下一個節點,那麼說明該位置上存在一個鏈表了(hash相同的多個元素以鏈表的方式存儲到
                    了老數組的這個位置上了)
                // 例如:數組長度爲16,那麼hash值爲1(1%16=1)的和hash值爲17(17%16=1)的兩個元素都是會存儲
                    在數組的第2個位置上(對應數組下標爲1),當數組擴容爲32(1%32=1)時,hash值爲1的還應該存儲
                    在新數組的第二個位置上,但是hash值爲17(17%32=17)的就應該存儲在新數組的第18個位置上了。
                // 所以,數組擴容後,所有元素都需要重新計算在新數組中的位置。


            else if (e instanceof TreeNode)  // 如果該節點爲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;  // 按命名來翻譯的話,應該叫高位首尾節點
                // 以上的低位指的是新數組的 0  到 oldCap-1 、高位指定的是oldCap 到 newCap - 1
                Node<K,V> next;
                // 遍歷鏈表
                do {  
                    next = e.next;
                    // 這一步判斷好狠,拿元素的hash值  和  老數組的長度  做與運算
                    // PS3裏曾說到,數組的長度一定是2的N次方(例如16),如果hash值和該長度做與運算,結果爲0,就
                    說明該hash值一定小於數組長度(例如hash值爲1),那麼該hash值再和新數組的長度取摸的話,還是
                    hash值本身,所該元素的在新數組的位置和在老數組的位置是相同的,所以該元素可以放置在低位鏈表
                    中。
                    if ((e.hash & oldCap) == 0) {  
                        // PS4
                        if (loTail == null) // 如果沒有尾,說明鏈表爲空
                            loHead = e; // 鏈表爲空時,頭節點指向該元素
                        else
                            loTail.next = e; // 如果有尾,那麼鏈表不爲空,把該元素掛到鏈表的最後。
                        loTail = e; // 把尾節點設置爲當前元素
                    }

                    // 如果與運算結果不爲0,說明hash值大於老數組長度(例如hash值爲17)
                    // 此時該元素應該放置到新數組的高位位置上
                    // 例:老數組長度16,那麼新數組長度爲32,hash爲17的應該放置在數組的第17個位置上,也就是下標爲
                    16,那麼下標爲16已經屬於高位了,低位是[0-15],高位是[16-31]
                    else {  // 以下邏輯同PS4
                        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; // 例:hash爲 17 在老數組放置在0下標,在新數組放置在16下標;    
                    hash爲 18 在老數組放置在1下標,在新數組放置在17下標;                   
                }
            }
        }
    }
}
return newTab; // 返回新數組
}

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