| 
 
 一、前言在上一章節(jié)我們講解并用數(shù)據(jù)驗證了,HashMap中的, 除了以上這些知識點外,HashMap還有基本的數(shù)據(jù)功能; 那么本章節(jié)會進行講解以下知識點; 
 ??注意: 建議閱讀上一篇后,再閱讀本篇文章《HashMap核心知識,擾動函數(shù)、負(fù)載因子、擴容鏈表拆分,深度學(xué)習(xí)》 二、HashMap源碼分析1. 插入1.1 疑問點&考題通過上一章節(jié)的學(xué)習(xí):《HashMap核心知識,擾動函數(shù)、負(fù)載因子、擴容鏈表拆分,深度學(xué)習(xí)》 大家對于一個散列表數(shù)據(jù)結(jié)構(gòu)的HashMap往里面插入數(shù)據(jù)時,基本已經(jīng)有了一個印象。簡單來說就是通過你的Key值取得哈希再計算下標(biāo),之后把相應(yīng)的數(shù)據(jù)存放到里面。 但再這個過程中會遇到一些問題,比如; 
 這些疑問點都會在后面的內(nèi)容中逐步講解,也可以自己思考一下,如果是你來設(shè)計,你會怎么做。 1.2 插入流程和源碼分析HashMap插入數(shù)據(jù)流程圖 visio原版流程圖,可以通過關(guān)注公眾號:bugstack蟲洞棧,進行下載 以上就是HashMap中一個數(shù)據(jù)插入的整體流程,包括了;計算下標(biāo)、何時擴容、何時鏈表轉(zhuǎn)紅黑樹等,具體如下; 
 JDK1.8 HashMap的put方法源碼如下: public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 初始化桶數(shù)組 table,table 被延遲到插入新數(shù)據(jù)時再進行初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 如果桶中不包含鍵值對節(jié)點引用,則將新鍵值對節(jié)點的引用存入桶中即可
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 如果鍵的值以及節(jié)點 hash 等于鏈表中的第一個鍵值對節(jié)點時,則將 e 指向該鍵值對
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
            
        // 如果桶中的引用類型為 TreeNode,則調(diào)用紅黑樹的插入方法
        else if (p instanceof TreeNode)  
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 對鏈表進行遍歷,并統(tǒng)計鏈表長度
            for (int binCount = 0; ; ++binCount) {
                // 鏈表中不包含要插入的鍵值對節(jié)點時,則將該節(jié)點接在鏈表的最后
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 如果鏈表長度大于或等于樹化閾值,則進行樹化操作
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                
                // 條件為 true,表示當(dāng)前鏈表包含要插入的鍵值對,終止遍歷
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        // 判斷要插入的鍵值對是否存在 HashMap 中
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // onlyIfAbsent 表示是否僅在 oldValue 為 null 的情況下更新鍵值對的值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 鍵值對數(shù)量超過閾值時,則進行擴容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;}1.3 擴容機制HashMap是基于數(shù)組+鏈表和紅黑樹實現(xiàn)的,但用于存放key值得的數(shù)組桶的長度是固定的,由初始化決定。 那么,隨著數(shù)據(jù)的插入數(shù)量增加以及負(fù)載因子的作用下,就需要擴容來存放更多的數(shù)據(jù)。而擴容中有一個非常重要的點,就是jdk1.8中的優(yōu)化操作,可以不需要再重新計算每一個元素的哈希值,這在上一章節(jié)中已經(jīng)講到,可以閱讀系列專題文章,機制如下圖; 里我們主要看下擴容的代碼(注釋部分); final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // Cap 是 capacity 的縮寫,容量。如果容量不為空,則說明已經(jīng)初始化。
    if (oldCap > 0) {
        // 如果容量達(dá)到最大1 << 30則不再擴容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        
        // 按舊容量和閥值的2倍計算新容量和閥值
        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
    
        // initial capacity was placed in threshold 翻譯過來的意思,如下;
        // 初始化時,將 threshold 的值賦值給 newCap,
        // HashMap 使用 threshold 變量暫時保存 initialCapacity 參數(shù)的值
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        // 這一部分也是,源代碼中也有相應(yīng)的英文注釋
        // 調(diào)用無參構(gòu)造方法時,數(shù)組桶數(shù)組容量為默認(rèn)容量 1 << 4; aka 16
        // 閥值;是默認(rèn)容量與負(fù)載因子的乘積,0.75
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
    // newThr為0,則使用閥值公式計算容量
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    
    @SuppressWarnings({"rawtypes","unchecked"})
        // 初始化數(shù)組桶,用于存放key
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        // 如果舊數(shù)組桶,oldCap有值,則遍歷將鍵值映射到新數(shù)組桶中
        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;
                else if (e instanceof TreeNode)
                    // 這里split,是紅黑樹拆分操作。在重新映射時操作的。
                    ((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;
                    // 這里是鏈表,如果當(dāng)前是按照鏈表存放的,則將鏈表節(jié)點按原順序進行分組{這里有專門的文章介紹,如何不需要重新計算哈希值進行拆分《HashMap核心知識,擾動函數(shù)、負(fù)載因子、擴容鏈表拆分,深度學(xué)習(xí)》}
                    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;}以上的代碼稍微有些長,但是整體的邏輯還是蠻清晰的,主要包括; 
 1.4 鏈表樹化HashMap這種散列表的數(shù)據(jù)結(jié)構(gòu),最大的性能在于可以O(shè)(1)時間復(fù)雜度定位到元素,但因為哈希碰撞不得已在一個下標(biāo)里存放多組數(shù)據(jù),那么jdk1.8之前的設(shè)計只是采用鏈表的方式進行存放,如果需要從鏈表中定位到數(shù)據(jù)時間復(fù)雜度就是O(n),鏈表越長性能越差。因為在jdk1.8中把過長的鏈表也就是8個,優(yōu)化為自平衡的紅黑樹結(jié)構(gòu),以此讓定位元素的時間復(fù)雜度優(yōu)化近似于O(logn),這樣來提升元素查找的效率。但也不是完全拋棄鏈表,因為在元素相對不多的情況下,鏈表的插入速度更快,所以綜合考慮下設(shè)定閾值為8才進行紅黑樹轉(zhuǎn)換操作。 鏈表轉(zhuǎn)紅黑樹,如下圖; 以上就是一組鏈表轉(zhuǎn)換為紅黑樹的情況,元素包括;40、51、62、73、84、95、150、161 這些是經(jīng)過實際驗證可分配到Idx:12的節(jié)點 通過這張圖,基本可以有一個 鏈表樹化源碼 final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 這塊就是我們上面提到的,不一定樹化還可能只是擴容。主要桶數(shù)組容量是否小于64 MIN_TREEIFY_CAPACITY 
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
    // 又是單詞縮寫;hd = head (頭部),tl = tile (結(jié)尾)
        TreeNode<K,V> hd = null, tl = null;
        do {
            // 將普通節(jié)點轉(zhuǎn)換為樹節(jié)點,但此時還不是紅黑樹,也就是說還不一定平衡
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            // 轉(zhuǎn)紅黑樹操作,這里需要循環(huán)比較,染色、旋轉(zhuǎn)。關(guān)于紅黑樹,在下一章節(jié)詳細(xì)講解
            hd.treeify(tab);
    }}這一部分鏈表樹化的操作并不復(fù)雜,復(fù)雜點在于下一層的紅黑樹轉(zhuǎn)換上,這部分知識點會在后續(xù)章節(jié)中專門介紹; 以上源碼主要包括的知識點如下; 
 1.5 紅黑樹轉(zhuǎn)鏈在鏈表轉(zhuǎn)紅黑樹中我們重點介紹了一句,在轉(zhuǎn)換樹的過程中,記錄了原有鏈表的順序。 那么,這就簡單了,紅黑樹轉(zhuǎn)鏈表時候,直接把TreeNode轉(zhuǎn)換為Node即可,源碼如下; final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    // 遍歷TreeNode
    for (Node<K,V> q = this; q != null; q = q.next) {
    // TreeNode替換Node
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;}// 替換方法Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
    return new Node<>(p.hash, p.key, p.value, next);}因為記錄了鏈表關(guān)系,所以替換過程很容易。所以好的數(shù)據(jù)結(jié)構(gòu)可以讓操作變得更加容易。 2. 查找上圖就是HashMap查找的一個流程圖,還是比較簡單的,同時也是高效的。 接下來我們在結(jié)合代碼,來分析這段流程,如下; public V get(Object key) {
    Node<K,V> e;
    // 同樣需要經(jīng)過擾動函數(shù)計算哈希值
    return (e = getNode(hash(key), key)) == null ? null : e.value;}final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 判斷桶數(shù)組的是否為空和長度值
    if ((tab = table) != null && (n = tab.length) > 0 &&
        // 計算下標(biāo),哈希值與數(shù)組長度-1
        (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) {
            // TreeNode 節(jié)點直接調(diào)用紅黑樹的查找方法,時間復(fù)雜度O(logn)
            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;
            } while ((e = e.next) != null);
        }
    }
    return null;}以上查找的代碼還是比較簡單的,主要包括以下知識點; 
 3. 刪除 public V remove(Object key) {
     Node<K,V> e;
     return (e = removeNode(hash(key), key, null, false, true)) == null ?
         null : e.value;
 }
 final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 定位桶數(shù)組中的下標(biāo)位置,index = (n - 1) & hash
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // 如果鍵的值與鏈表第一個節(jié)點相等,則將 node 指向該節(jié)點
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            // 樹節(jié)點,調(diào)用紅黑樹的查找方法,定位節(jié)點。
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // 遍歷鏈表,找到待刪除節(jié)點
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        
        // 刪除節(jié)點,以及紅黑樹需要修復(fù),因為刪除后會破壞平衡性。鏈表的刪除更加簡單。
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                tab[index] = node.next;
            else
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;}
 4. 遍歷4.1 問題點HashMap中的遍歷也是非常常用的API方法,包括; KeySet  for (String key : map.keySet()) {
     System.out.print(key + " ");
 }EntrySet  for (HashMap.Entry entry : map.entrySet()) {
     System.out.print(entry + " ");
 }從方法上以及日常使用都知道,KeySet是遍歷是無序的,但每次使用不同方式遍歷包括 那么從實現(xiàn)的角度來看,這些種遍歷都是從散列表中的鏈表和紅黑樹獲取集合值,那么他們有一個什么固定的規(guī)律嗎? 4.2 用代碼測試測試的場景和前提; 
 代碼測試 @Testpublic void test_Iterator() {
    Map<String, String> map = new HashMap<String, String>(64);
    map.put("24", "Idx:2");
    map.put("46", "Idx:2");
    map.put("68", "Idx:2");
    map.put("29", "Idx:7");
    map.put("150", "Idx:12");
    map.put("172", "Idx:12");
    map.put("194", "Idx:12");
    map.put("271", "Idx:12");
    System.out.println("排序01:");
    for (String key : map.keySet()) {
        System.out.print(key + " ");
    }
    
    map.put("293", "Idx:12");
    map.put("370", "Idx:12");
    map.put("392", "Idx:12");
    map.put("491", "Idx:12");
    map.put("590", "Idx:12");
    System.out.println("\n\n排序02:");
    for (String key : map.keySet()) {
        System.out.print(key + " ");
    }    
    
    map.remove("293");
    map.remove("370");
    map.remove("392");
    map.remove("491");
    map.remove("590");
    System.out.println("\n\n排序03:");
    for (String key : map.keySet()) {
        System.out.print(key + " ");
    }
    }這段代碼分別測試了三種場景,如下; 
 4.3 測試結(jié)果分析排序01:24 46 68 29 150 172 194 271 排序02:24 46 68 29 271 150 172 194 293 370 392 491 590 排序03:24 46 68 29 172 271 150 194 Process finished with exit code 0 從map.keySet()測試結(jié)果可以看到,如下信息; 
 
 
 三、總結(jié)
 四、推薦閱讀
 | 
|  |