国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

JDK源碼那些事兒之我眼中的HashMap

voyagelab / 2708人閱讀

摘要:接下來就來說下我眼中的。鏈表轉換為樹的閾值,超過這個長度的鏈表會被轉換為紅黑樹,當然,不止這一個條件,在下面的源碼部分會看到。

源碼部分從HashMap說起是因為筆者看了很多遍這個類的源碼部分,同時感覺網上很多都是粗略的介紹,有些可能還不正確,最后只能自己看源碼來驗證理解,寫下這篇文章一方面是為了促使自己能深入,另一方面也是給一些新人一些指導,不求有功,但求無過。有錯誤的地方請在評論中指出,我會及時驗證修改,謝謝。

接下來就來說下我眼中的HashMap。

jdk版本:1.8

在深入源碼之前,了解HashMap的整體結構是非常重要的事情,結構也體現(xiàn)出了源碼中一些對HashMap的操作,結構大致如下:

從上邊的結構圖大家應該也能看出來HashMap的實現(xiàn)結構:數(shù)組+鏈表+紅黑樹

看下類注釋,直接看源碼部分最好,可能大多數(shù)都看不明白,這里可以看下別人的翻譯:類注釋翻譯。本文中筆者不打算對紅黑樹部分進行講解說明,插入和刪除操作會引發(fā)各種狀態(tài),需要做對應的調整,之后會多帶帶寫一篇紅黑樹基礎,結合TreeNode來做講解。

先總結一些名詞概念方便初學者理解:

1.桶(bucket):數(shù)組中存儲元素的位置,參考結構圖,實際上是數(shù)組中的某個索引下的元素,這個元素有可能是樹的根節(jié)點或者鏈表的首節(jié)點,當然,理解上還是一個鏈表或紅黑樹整體當成桶

2.bin:桶中的每個元素,即紅黑樹中的某個元素或者是鏈表中的某個元素。

除了上邊的名詞,最好還能去理解下哈希表,可以參考下。HashMap也是對哈希表的一種實現(xiàn),簡單理解,可以類比數(shù)學中的求余操作,對范圍進行固定,將大量的數(shù)據放入一個有界的范圍中,求余放置,這種操作算是哈希表的一種實現(xiàn)方式。

下面進行源碼部分的說明:

類定義

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable

繼承AbstractMap
實現(xiàn)Cloneable接口,提供克隆功能
實現(xiàn)Serializable接口,支持序列化,方便序列化傳輸

這里有個有意思的問題:為什么HashMap繼承了AbstractMap還要實現(xiàn)Map接口?有興趣的可以去看下stackoverflow上的回答:

https://stackoverflow.com/que...

變量說明
    /**
     * Node數(shù)組的默認長度,16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * Node數(shù)組的最大長度,最大擴容長度
     */
 
    /**
     * 默認負載因子
     * 這個是干嘛的呢?
     * 負載因子是哈希表在自動擴容之前能承受容量的一種尺度。
     * 當哈希表的數(shù)目超出了負載因子與當前容量的乘積時,則要對該哈希表進行rehash操作(擴容操作)。
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 鏈表轉換為樹的閾值,超過這個長度的鏈表會被轉換為紅黑樹,
     * 當然,不止這一個條件,在下面的源碼部分會看到。
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 當進行resize操作時,小于這個長度的樹會被轉換為鏈表       
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 鏈表被轉換成樹形的最小容量,
     * 如果沒有達到這個容量只會執(zhí)行resize進行擴容
     * 可以理解成一種計算規(guī)則
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
    /**
     * 
     * 第一次使用的時候進行初始化,put操作才會初始化對象
     * 調用構造函數(shù)時不會初始化,后面源碼可參考
     */
    transient Node[] table;

    /**
     * 
     * entrySet保存key和value 用于迭代
     */
    transient Set> entrySet;

    /**
     * 
     * 存放元素的個數(shù),但不等于數(shù)組的長度
     */
    transient int size;

    /**
     * 
     * 計數(shù)器,fail-fast機制相關,不詳細介紹,有興趣的自己google下
     * 你可以當成一個在高并發(fā)讀寫操作時的判斷,舉個例子:
     * 一個線程A迭代遍歷a,modCount=expectedModCount值為1,執(zhí)行過程中,一個線程B修改了a,modCount=2,線程A在遍歷時判斷了modCount<>expectedModCount,拋錯
     * 當然,這個只是簡單的檢查,并不能得到保證
     */
    transient int modCount;

    /**
     * 
     * 閾值,當實際大小超過閾值(容量*負載因子)的時候,會進行擴容
     */
    int threshold;

    /**
     * 
     * 負載因子
     */
    final float loadFactor;

在看方法之前先看下Node實現(xiàn):

    /**
     * Node的實現(xiàn) 
     * 看出來是最多實現(xiàn)單向鏈表 僅有一個next引用
     * 比較簡單明了,應該都能看明白
     */
    static class Node implements Map.Entry {
        final int hash;
        final K key;
        V value;
        Node next;

        Node(int hash, K key, V value, Node 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;
        }
        
        /**
         * Map.Entry 判斷類型
         * 鍵值對進行比較 判斷是否相等
         */
        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;
        }
    }
重點說明

在注釋中我會添加一些標記幫助理清流程,同時方便我后邊總結對照和參考(例如A1,A2是同一級)。

    /**
     * 負載因子設置成默認值 0.75f
     * A1
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    /**
     * 初始數(shù)組長度設置,負載因子默認值
     * A2
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    /**
     * 初始長度和負載因子設置
     * A2
     */   
    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;
        // 根據初始容量設置閾值
        // 二進制操作,比較繞,需要自己好好理解下
        // 這值在resize有用,resize代碼可以注意下,主要是為了區(qū)分是否是有參構造函數(shù)還是無參構造函數(shù)以便之后的操作
        // 可以參考文章:https://www.cnblogs.com/liujinhong/p/6576543.html
        // 是否有更深層次的考慮筆者還未想到,有大神可以在評論區(qū)告知我
        this.threshold = tableSizeFor(initialCapacity);
    }
    /**
     * 將m存入當前map中
     */  
    public HashMap(Map m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    /**
     * evict參數(shù)相當于占位符,是為了擴展性,可以追溯到afterNodeInsertion(evict),方法是空的
     * 在LinkedHashMap中有實現(xiàn),有興趣可以去看看
     */  
    final void putMapEntries(Map m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            /**
             * 判斷table是否已經被初始化
             */
            if (table == null) { // pre-size
                // 未被初始化,判斷m中元素的個數(shù)放入當前map中是否會超出最大容量的閾值
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                // 計算得到的t大于閾值 閾值設置
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                /**
                 * 當前map已經初始化,并且添加的元素長度大于閾值,需要進行擴容操作
                 */  
                resize();
            /**
             * 上邊已經初始化并處理好閾值設置,下面使用entrySet循環(huán)putVal保存m中的Node對象的key和value
             * 這里有個重要的地方,
             * putVal的第一個參數(shù),hash(key),map的put操作也是同樣的調用方式
             * 可以參考文章:https://www.cnblogs.com/liujinhong/p/6576543.html
             * 順便看下源碼上的注釋,主要是減少沖突和性能上的考慮
             */  
            for (Map.Entry e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }
    
    /**
     * 擴容操作,重點部分
     * 
     * 如果第一次帶容量參數(shù)時,創(chuàng)建時閾值設置為對應容量的最小的2的N次方(大于等于傳入容量參數(shù)),去看下上邊HashMap(int initialCapacity),
     * 如果添加一個元素,會執(zhí)行resize將閾值設置為了閾值 * 負載因子,
     * 比如設置1000 創(chuàng)建時閾值threshold=1024,負載因子默認,其他值都未進行操作,
     * 添加一個元素 閾值變?yōu)?024 * 0.75 = 768,創(chuàng)建的Node數(shù)組長度為1024,size=1,
     * 添加第769個元素時,進行resize操作,threshold=1536,Node數(shù)組長度為2048,數(shù)組拷貝到新數(shù)組中,
     * 如果有確認的數(shù)據長度,不想讓HashMap進行擴容操作,那么則需要在構造時填上計算好的數(shù)組容量
     * 強烈建議自己寫代碼debug試試
     */
    final Node[] resize() {
        //oldTab 保存擴容前的Node數(shù)組
        Node[] oldTab = table;
        // oldCap null的話即為0,否則就是擴容前的Node數(shù)組的容量大小
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 擴容前的閾值
        int oldThr = threshold;
        // 擴容后的數(shù)組容量(長度),擴容后的閾值
        int newCap, newThr = 0;
        // 1.擴容前的數(shù)組不為空
        // B1
        if (oldCap > 0) {
            // 擴容前的Node數(shù)組容量大于等于設置的最大容量,不會進行擴容,閾值設置為Integer.MAX_VALUE
            if (oldCap >= MAXIMUM_CAPACITY) {
                // C1
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 如果擴容前的數(shù)組容量擴大為2倍依然沒有超過最大容量,
            // 并且擴容前的Node數(shù)組容量大于等于數(shù)組的默認容量,
            // 擴容后的數(shù)組容量值為擴容前的map的容量的2倍,并且擴容后的閾值同樣設置為擴容前的兩倍,
            // 反之,則只設置擴容后的容量值為擴容前的map的容量的2倍
            // 這里newCap已經在條件里賦值了
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                // C2
                newThr = oldThr << 1; // double threshold
        }
        // 2.擴容前的數(shù)組未初始化并且使用了有參構造函數(shù)構造
        // 這里在oldCap = 0時執(zhí)行,這里oldThr > 0說明初始化時是有參初始化構造的map
        // 自己可以試下無參構造函數(shù),threshold的值為0
        // B2
        else if (oldThr > 0) // initial capacity was placed in threshold
            // 使用有參初始化構造函數(shù)并且在第一次put操作時會進入執(zhí)行(去看下put源碼)
            // 擴容后的容量大小設置為原有閾值
            // 例如我上邊的注釋中的例子,這里第一次添加鍵值對時容量設置為了1024
            newCap = oldThr;
        // 3.擴容前的數(shù)組未初始化并且使用了無參構造函數(shù)構造
        // B3
        else {               // zero initial threshold signifies using defaults
            // 擴容后的容量 = 默認容量,擴容后的閾值 = 默認容量 * 負載因子
            // 擴容后的容量為16,閾值為12
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        /**
         * 上邊設置了新容量和新的閾值,執(zhí)行到這里,你應該發(fā)現(xiàn)只有newThr可能沒被賦值,所以這里要繼續(xù)進行一個操作,來對newThr進行賦值
         * 新閾值等于0,照上邊邏輯:
         * 兩種情況:
         * 1.擴容前的node數(shù)組容量有值且擴容后容量超過最大值或者原node數(shù)組容量小于默認初始容量16
         * 2.使用有參構造函數(shù),第一次put操作時上邊代碼里沒有設置newThr
         * D1
         */
        if (newThr == 0) {
            // 應該得到的新閾值ft = 新容量 * 負載因子
            float ft = (float)newCap * loadFactor;
            // 假如新容量小于最大容量并且ft小于最大容量則新的閾值設置為ft,否則設置成int最大值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        // 執(zhí)行到這,擴容后的容量和閾值都計算完畢
        // 閾值設置為新閾值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        // 創(chuàng)建擴容后的Node數(shù)組 
        Node[] newTab = (Node[])new Node[newCap];
        // 切換為擴容后的Node數(shù)組,此時還未進行將舊數(shù)組拷貝到新數(shù)組
        table = newTab;
        // E1
        if (oldTab != null) {
            // 原有數(shù)組不為空,將原有數(shù)組數(shù)據拷貝到新數(shù)組中
            for (int j = 0; j < oldCap; ++j) {
                Node e;
                // 非空元素才進行賦值
                if ((e = oldTab[j]) != null) {
                    // 原有數(shù)值引用置空,方便GC
                    oldTab[j] = null;
                    if (e.next == null)
                        // 桶對應的Node只有當前一個節(jié)點,鏈表長度為1
                        // 中括號中計算原有數(shù)組元素在新數(shù)組中存放的位置,
                        // 為什么這么計算?
                        // 正常的想,添加了一個鍵值對,鍵的hash值(當然,這里在HashMap的hash(key)進行了統(tǒng)一處理)
                        // 那么長度是有限的,在這個有限長度下如何放置,類比整數(shù)取余操作,
                        // &操作表明只取e.hash的低n位,n是newCap - 1轉換成二進制的有效位數(shù)
                        // 這里記得初始不設長度時默認16,二進制為10000,減一為1111,低4位
                        // 設置長度時tableSizeFor重新設置了長度和16處理類似
                        // 通過&操作所有添加的鍵值對都分配到了數(shù)組中,當然,分配到數(shù)組中同一個位置時會擴展成鏈表或紅黑樹
                        // 添加詳細操作看后邊putVal源碼,這里先不用糾結
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        // 到此說明e.next不為空,那么需要判斷了,
                        // 因為有兩種結構,一種是鏈表,一種為紅黑樹
                        // 這里先進行紅黑樹處理,樹的具體處理后邊有時間多帶帶做一章進行說明講解,
                        // 這里先簡單了解,擴容之后,需要對原有的樹進行處理,使得數(shù)據分散比較均勻。
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        /**
                         * 到這里結合HashMap的結構,
                         * 排除上邊兩個條件,這里就進行鏈表結構的處理
                         * 進行鏈表復制操作
                         * 復制的時候就有個問題了,舉個例子,原來我是16,現(xiàn)在擴容成了32(原數(shù)組兩倍,我上邊分析里有說明)
                         * 那么我復制時怎么辦?
                         * 不移動原來的鏈表?
                         * 這里就要想到了我擴容之后訪問的時候不能影響
                         * 那么就需要看下put操作時是怎樣存的,這里先說下,putVal里也可以看到
                         * (n - 1) & hash 和上邊newTab[e.hash & (newCap - 1)] 分析是一樣的
                         * 這里不知道你想到了嗎?擴容之后有什么不同?
                         * 如果還沒什么想法,請繼續(xù)往下看,我等下會說明
                         * 新擴容部分頭尾節(jié)點(hi可以理解成高位)設置為hiHead,hiTail
                         * 原有部分頭尾節(jié)點(lo可以理解成低位)設置為loHead,loTail
                         * 這里什么意思呢?
                         * 往下看就好,我下面的注釋詳細說明了為什么定義了兩個鏈表頭尾節(jié)點
                         */
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node next;
                        // 這里循環(huán)獲取鏈表元素進行處理
                        do {
                            next = e.next;
                            /**
                             * e.hash & oldCap = 0
                             * 位與操作,這里初學者要自己寫下多理解下
                             * 舉個例子:
                             * oldCap=32=100000(二進制),newCap=64=1000000(二進制)
                             * 在未擴容之前計算元素所處位置是(oldCap-1) & hash
                             * 全1位與操作,取值范圍落在0~oldCap-1
                             * e.hash & oldCap 只判斷了最高位的那個1位置是否相同
                             * 相同則非0,不同則為0
                             * 為什么要判斷這一位呢?
                             * 我們想一想,擴容之后,計算bucket(桶)位置(即元素落在數(shù)組那個索引位置)時
                             * (newCap-1) & hash和(oldCap-1) & hash兩者對比,只有一位不同
                             * 比如32和64,最高位是1不同,其他位相同
                             * 如果擴容之后最高位為0,則擴容前后得到的bucket位置相同,不需要調整位置
                             * 如果非0,則是1,則需要將桶位置調整到更高的索引位置
                             * 而且這里也應該明白,同一個bucket下的鏈表(非單一元素)在擴容后
                             * 因為只有一位二進制不同,不是1就是0
                             * 最多分到兩個bucket中,一個是擴容前的bucket(當前所在的bucket),
                             * 一個是擴容后的bucket(新的bucket),
                             * 這里也說明了上邊為什么設置了兩組頭尾節(jié)點,一組低位鏈表,一組高位鏈表
                             * 擴容前后兩個bucket位置之間差值為原數(shù)組容量值
                             * 上邊32和64,差值為63-31=32=oldCap=10000(二進制)
                             * 所以這下面使用的是oldCap
                             */
                            if ((e.hash & oldCap) == 0) {
                                // 說明當前Node元素位置 = 原數(shù)組中的位置
                                // 放入loHead,loTail這一組中,低位鏈表
                                if (loTail == null)
                                    // 鏈表還未放元素,鏈表頭賦值
                                    loHead = e;
                                else
                                    // 鏈表存在元素,新元素放置在鏈表尾部,next指向新元素
                                    loTail.next = e;
                                // 尾節(jié)點指向改變,變成了新添加的節(jié)點
                                loTail = e;
                            }
                            else {
                                // 類似上邊
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        
                        // 上面已經處理完了,分成了高低位兩個鏈表,下面就是將這兩個鏈表放置擴容后的新數(shù)組中
                        if (loTail != null) {
                            // 低位鏈表不為空,添加到新數(shù)組,尾節(jié)點next指向置空,因為原有節(jié)點可能還存在next指向
                            loTail.next = null;
                            // 新數(shù)組j處就是原有數(shù)組j處,這里直接將低位首節(jié)點引用賦值給新數(shù)組節(jié)點
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            // 這里和我上邊注釋分析是一致的,相差的值即為oldCap,即原數(shù)組的容量
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

    /**
     * put操作方法主體
     * hash,key的hash值,上邊講過,HashMap自己處理過的
     * onlyIfAbsent,是否覆蓋原有值,true,不覆蓋原有值
     * evict,LinkedHashMap實現(xiàn)afterNodeInsertion方法時調用,這里相當于占位符的作用
     */    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        // F1
        if ((tab = table) == null || (n = tab.length) == 0)
            // table為空或長度為0時,對table進行初始化,上邊已經分析過了
            // 這里也說明了第一次初始化是在這里,而不是使用構造方法,排除putMapEntries方式
            n = (tab = resize()).length;
        // 判斷當前需要存儲的鍵值對存放到數(shù)組中的位置是否已經存在值(鏈表或者紅黑樹)
        // 即是否已經有對應key
        // G1
        if ((p = tab[i = (n - 1) & hash]) == null)
            // 不存在,則創(chuàng)建一個新節(jié)點保存
            tab[i] = newNode(hash, key, value, null);
        // G2
        else {
            // 將桶上的值進行匹配,判斷是否存在
            Node e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // 鏈表頭節(jié)點(或紅黑樹根節(jié)點)與當前需要保存的hash值相等
                // 并且key值相等,e和p是同一個,說明添加了相同的key
                // e指向p對應的節(jié)點
                e = p;
            else if (p instanceof TreeNode)
                // 紅黑樹添加節(jié)點處理,本文不詳細將紅黑樹部分,后面有空會多帶帶抽出講解
                // 返回值可以理解成如果有相同key,則返回對應Node,否則返回null(創(chuàng)建了新的Node)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 這里說明非頭節(jié)點(數(shù)組中對應的桶的第一個節(jié)點),非紅黑樹結構,
                // 說明需要匹配鏈表,判斷鏈表中對應的key是否已存在
                // 設置binCount計算當前桶中bin的數(shù)量,即鏈表長度
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        // next 為空 無下一個元素 不再繼續(xù)查找 直接新創(chuàng)建直接賦值next
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            // 判斷是否樹化,這里就是鏈表樹化條件,在treeifyBin還有個數(shù)組容量判斷,方法也可能只進行擴容操作
                            // 總結下,即桶中bin數(shù)量大于等于TREEIFY_THRESHOLD=8,數(shù)組容量不能小于MIN_TREEIFY_CAPACITY=64時進行樹化轉化
                            // 怎么轉成紅黑樹結構這里也不做深入,后續(xù)會進行說明
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 不為空 且節(jié)點為尋找的節(jié)點 終止循環(huán)
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 上邊已經檢查完map中是否存在對應key的Node節(jié)點,不存在的新創(chuàng)建節(jié)點,這里處理下存在對應key的節(jié)點數(shù)據
            // H1
            if (e != null) { // existing mapping for key
                // 保存下原來的節(jié)點值
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    // onlyIfAbsent 是否需要覆蓋操作,是則覆蓋
                    e.value = value;
                // 子類實現(xiàn)方法的話可以進行對應的后置操作
                afterNodeAccess(e);
                // 返回原值
                return oldValue;
            }
        }
        ++modCount;
        // 實際元素長度,不是容量,是每次添加一個新的鍵值對會加1,覆蓋不增加
        // 判斷是否大于閾值,進行擴容操作
        // I1
        if (++size > threshold)
            resize();
        // 同afterNodeAccess,子類實現(xiàn)方法的話可以進行對應的后置操作
        afterNodeInsertion(evict);
        return null;
    }

重點的部分也就是在上面這幾個方法,剩下的源碼部分就不一一貼出來分析了,能看懂我上面說明的部分,基本上除了紅黑樹和jdk1.8的新特性相關部分,其余部分應該基本都能看懂,這里再補充一個序列化方面的問題:

為什么HashMap中的table變量要設置為transient?在理解這個問題之前,自行去看下序列化代碼writeObject和readObject,然后參考以下鏈接來思考:

https://segmentfault.com/q/10...

HashMap中,由于Entry的存放位置是根據Key的Hash值來計算,然后存放到數(shù)組中的,對于同一個Key,在不同的JVM實現(xiàn)中計算得出的Hash值可能是不同的。這里不同意思是說我原來在window機器上A是放在Node數(shù)組中0的位置,在Mac上可能是放在Node數(shù)組中5的位置,但是不修改的話,反序列化之后Mac上也是0的位置,這樣導致后續(xù)增加節(jié)點會錯亂,不是我們想要的結果,故在序列化中HashMap對每個鍵值對的鍵和值序列化,而不是整體,反序列化一個一個取出來,不會造成位置錯亂。

那么JDK1.8中HashMap在多線程環(huán)境下會造成死循環(huán)嗎?

從上邊結構以及處理過程的分析來看,應該是不會的,只不過數(shù)據丟失還是會發(fā)生,這一塊我就不進行驗證了,自行Google,手寫代碼來驗證。同時想多說句,對于一般開發(fā)人員知道HashMap是非線程安全的,多線程情況下使用ConcurrentHashMap即可,后邊有時間ConcurrentHashMap的分析我也會整理出來。

總結

在重點說明部分我已經詳細解釋了resize和put操作的過程,可能有些新人還是不能梳理清楚,我在這里結合下日常使用總結下整個過程,方便各位理解:

1.HashMap創(chuàng)建過程(正常狀態(tài)):

2.HashMap resize過程(正常狀態(tài)):

3.HashMap put過程(正常狀態(tài)):

HashMap首先需要理解清楚其內部的實現(xiàn)結構:數(shù)組+鏈表+紅黑樹,在結構的基礎之上來對源碼進行深入,resize和put操作是最為重要的兩部分,理解了這兩塊,基本上對HashMap的整體處理過程有了一定的認知,另外,一定要自己動手debug,理清數(shù)據的轉換,對了解HashMap有很大的幫助。

文章先從基礎部分說起,解釋了一些名詞,提及了哈希表,從實現(xiàn)結構開始來幫助各位更好的理解源碼操作部分,對重點的幾個部分做出詳細的說明,resize和put操作難點部分也做了相應的解釋,希望對各位有所幫助,后邊有空我會將紅黑樹部分的理解分享出來,謝謝。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/73593.html

相關文章

  • JDK源碼那些事兒HashMap.TreeNode

    摘要:前言版本以為例是因為之前的紅黑樹操作在文章省略了,這里進行一個解釋,其實源碼里并不是只有這個地方用紅黑樹結構,但是總體上都大同小異,故只說明這一部分就好,舉一反三的能力相信各位都應該擁有。紅黑樹類型遞歸左右子樹遍歷,直到值相等。 前面幾篇文章已經講解過HashMap內部實現(xiàn)以及紅黑樹的基礎知識,今天這篇文章就講解之前HashMap中未講解的紅黑樹操作部分,如果沒了解紅黑樹,請去閱讀前面...

    Pandaaa 評論0 收藏0
  • JDK源碼那些事兒之并發(fā)ConcurrentHashMap上篇

    前面已經說明了HashMap以及紅黑樹的一些基本知識,對JDK8的HashMap也有了一定的了解,本篇就開始看看并發(fā)包下的ConcurrentHashMap,說實話,還是比較復雜的,筆者在這里也不會過多深入,源碼層次上了解一些主要流程即可,清楚多線程環(huán)境下整個Map的運作過程就算是很大進步了,更細的底層部分需要時間和精力來研究,暫不深入 前言 jdk版本:1.8 JDK7中,ConcurrentH...

    Leck1e 評論0 收藏0
  • JDK源碼那些事兒之常用ArrayList

    摘要:前面已經講解集合中的并且也對其中使用的紅黑樹結構做了對應的說明,這次就來看下簡單一些的另一個集合類,也是日常經常使用到的,整體來說,算是比較好理解的集合了,一起來看下前言版本類定義繼承了,實現(xiàn)了,提供對數(shù)組隊列的增刪改查操作實現(xiàn)接口,提供隨 前面已經講解集合中的HashMap并且也對其中使用的紅黑樹結構做了對應的說明,這次就來看下簡單一些的另一個集合類,也是日常經常使用到的ArrayL...

    hizengzeng 評論0 收藏0
  • JDK源碼那些事兒之并發(fā)ConcurrentHashMap下篇

    摘要:上一篇文章已經就進行了部分說明,介紹了其中涉及的常量和變量的含義,有些部分需要結合方法源碼來理解,今天這篇文章就繼續(xù)講解并發(fā)前言本文主要介紹中的一些重要方法,結合上篇文章中的講解部分進行更進一步的介紹回顧下上篇文章,我們應該已經知道的整體結 上一篇文章已經就ConcurrentHashMap進行了部分說明,介紹了其中涉及的常量和變量的含義,有些部分需要結合方法源碼來理解,今天這篇文章就...

    Zack 評論0 收藏0
  • JDK源碼那些事兒之紅黑樹基礎下篇

    摘要:強調一下,紅黑樹中的葉子節(jié)點指的都是節(jié)點。故刪除之后紅黑樹平衡不用調整。將達到紅黑樹平衡。到此關于紅黑樹的基礎已經介紹完畢,下一章我將就源碼中的進行講解說明,看一看紅黑樹是如何在源碼中實現(xiàn)的。 說到HashMap,就一定要說到紅黑樹,紅黑樹作為一種自平衡二叉查找樹,是一種用途較廣的數(shù)據結構,在jdk1.8中使用紅黑樹提升HashMap的性能,今天就來說一說紅黑樹,上一講已經給出插入平衡...

    羅志環(huán) 評論0 收藏0

發(fā)表評論

0條評論

voyagelab

|高級講師

TA的文章

閱讀更多
最新活動
閱讀需要支付1元查看
<