HashMap源碼閱讀筆記

39-45行 註釋:主要介紹了HashMap和HashTable的區別,即HashMap允許null作爲鍵、值,而且HashMap不是線程安全的。並且HashMap中的元素不是有序的,特別的,也不保證隨着時間推移,這個map中存儲的順序不發生改變。

解析:hashmap是線程不安全的,鍵、值都允許null的存在,map中的元素不保證有序,隨着時間推移可能還會發生變化。


47-54行 註釋:HashMap的get、put方法在能夠正確將元素散列在桶中的情況下擁有常數級別的表現(即碰撞次數不過多)。而迭代器在Map中的表現情況依賴於初始化的容量與桶的數量+鍵值對的值的比例,即裝載因子。因此,如果希望迭代器的表現良好,則不能夠設置過大的初始容量。 

我們知道HashMap的方法中,默認的裝載因子大小是0.75,初始容量大小是16(旁邊還有一句註釋,必須是2的次方)

解析:默認裝載因子是0.75,默認初始容量是16


56-65行 註釋:就像上面提到的,影響HashMap性能的主要有兩個因素,一個是初始容量的大小,一個是裝載因子的大小。當HashMap中鍵值對的數量超過當前容量大小*裝載因子的時候,整個表會重新進行hash一次,並進行擴容(接近於原容量的兩倍)。

解析:每次resize的大小是將原容量擴充接近1倍。

67-76行 註釋: 介紹了採用0.75作爲默認裝載因子的意義。太高的裝載因子會減少多餘的空間,但是對查找性能很不友好。因此,最好是在初始化HashMap的時候根據它的鍵值對量對初始容量和裝載因子進行相應的調整,確保讓整個hashmap重新hash的次數最小化(即提高性能)。如果初始容量*裝載因子>鍵值對的數量,那麼重新散列的情況就不會發生。

解析:定


78-85行 註釋:如果Hashmap中的大多數鍵值對都已經有序了,那麼給與它一個充分大的容量會比小的好(肯定啊。。。小了又要rehash)。如果說map中存放了大量的重複的鍵,會影響map的效率。爲了改善影響,那麼會在鍵之間採用比較來改善這種情況。

解析:hashmap的散列方法採用的是拉鍊法,如果有大量重複主鍵的話,必然會導致碰撞的大量發生。


87-94行 註釋:介紹了hashmap的一個特點:線程不安全。如果有多個線程對Map進行增加/刪除元素的話,需要在外部對map對象加鎖。這個通常是由一些對象封裝了相應的映射關係來完成的。

解析:hashmap是線程不安全的,如果有併發操作需要在外部加上互斥鎖。


96-100行 註釋:接着上面的繼續講,如果不存在這種封裝了的對象,那麼hashmap需要在初始化之初使用一個包裝類:Collections.synchronizedMap

操作像這樣:Map m = Collections.synchronizedMap(new HashMap(...));

解析:一種使線程不安全的hashmap變得線程安全的操作。


102-109行 註釋:這裏介紹了一下集合類迭代器的一個共同特性:快速失敗。即當迭代器創建之後,所屬的集合有任何結構上的變化(比如增加/刪除元素,那種改變已有鍵值對的不算),都會拋出一個名爲:ConcurrentModificationException的異常。除非是使用迭代器本身安全的remove()方法。

解析:迭代器本身有兩個元素,一個ModCount,一個ExceptModCount,每次移動迭代器指針都會檢查這兩個的值是否相等,如果不等就會拋出快速失敗的異常,而迭代器本身的人remove()方法會同步修改這兩個值,則不會拋出異常。這個是所有集合類迭代器的共同特徵。

110-117行 註釋:迭代器的快速失敗特徵是不可靠的,應該說任何依賴於不同步情況下的併發修改都是難以保證正確性的。因此,迭代器的快速失敗特徵應該只被用於檢查bug出在哪裏,而不是保證程序的正確性。


145-154行 註釋:hashmap雖然在很多實現的性能表現地像哈希表元素存儲在桶裏,但在map中桶的數量過多時,桶節點會轉化成二叉樹節點(紅黑樹)我們知道紅黑樹是平衡樹的一種,它在數據很大時的查找性能很好。當然我們知道,方法大多數還是爲一般情況下準備的,因此這種檢驗桶是否變成了樹節點會一定程度上影響性能。

解析:HashMap在桶(一個單鏈表,因爲採用的是拉鍊法)的鍵簇(即鏈表中的元素數目)較大時(這個值是8),桶節點會變成樹節點(紅黑樹),用於提高它的查找性能。但同時這種檢驗也難免會一定程度上影響平時的性能。(最好的查找性能是一個桶裏只有一個元素,因爲桶裏存放的是發生碰撞的)


156-172行 註釋:當桶裏的節點全部變成樹節點的時候,它們會主要通過比較hashcode變得有序。如果兩個鍵之間都是實現了Comparable接口的(比如常用的原始數據類型+包裝類+String都是實現了的),那麼它們會通過compareTo()方法進行比較。雖然說這樣對於本來hashcode就唯一/已經有序的鍵值對會比較浪費時間,但是對於hashcode()方法錯誤的分配以及很多鍵共享同一個hashcode的情況,這麼做是值得的。

解析:對於樹節點中的數據,hashmap主要採用比較Hashcode的方法,如果鍵的類型實現了Comparable接口的方法,那麼就會採用compareTo()使它有序。我們知道,hashcode相等,不一定equals(),equals()不一定==。


174-186行 註釋:因爲樹節點佔用的空間比較大,近似普通節點的2倍,所以只有當空間足夠的情況下才會進行轉變(>=8),在小於6的時候又會轉變回去。如果hashcode方法分配良好,在理想情況下,桶中轉變爲樹節點的概率應當服從泊松分佈。

解析:轉化爲樹節點的閾值是8,退化的閾值是6.樹節點佔用空間較大。


199-202行 註釋:根節點有時候可能不在樹中(比如迭代器的remove方法),不過可以通過TreeNode.root()方法恢復。


204-209行 註釋:所有適用的內部方法接收哈希碼作爲一個參數(通常來自公有方法),來避免對鍵哈希碼的重新計算。許多內部方法也接收一個標籤參數,通常是當前的表,在resize的時候也能是新的或舊的表。

解析:接收哈希碼作爲參數,避免重複計算。提高性能。


211-218行 註釋:當哈希桶成樹型/非樹型以及分隔時,我們會保持它們處在同一種遍歷順序當中,並且一定程度上(較小)減輕迭代器操作的負擔。在使用比較和插入的時候,爲了保持總體的跨平衡有序,我們把鍵的類型和獨有的hashcode作爲連接橋樑。


220-226行 註釋:桶是樸素桶還是紅黑樹桶的使用和轉換,由於子類LinkedHashMap的存在而變得更加的複雜。hashmap中的一些由添加/刪除觸發得到鉤子方法允許LinkedHashMap在其他情況下保留獨立的內部特性。(筆者找到的鉤子方法中的一個是void reinitialize()方法)它們也需要map對象實例通過一些實例方法創建新的節點

(

228-229行 註釋:併發編程使用類似於SSA的風格,可以減少錯誤。(SSA:筆者查了一下,這是一種應用在JVM中的對代碼的編譯方法)

以上是hashmap的一些特性,接下來是源碼的逐行分析閱讀

232-235行 常量定義:默認的初始容量=16

237-242行 常量定義:hashmap的最大容量:1073741824(2^30) 必須是2的次方,如果在構造方法中隱式指定了更大的值,那麼只會採用這個值。

244-247行 常量定義:默認裝載因子=0.75

249-257行 常量定義:hash桶從鏈式存儲轉變成紅黑樹存儲的閾值=8
解析:這個值必須>=2,並且至少爲8,因爲要應對節點刪除導致又變回樸素桶的情況

259-264行 常量定義:hash桶由樹桶轉換爲樸素桶的閾值=6
解析:在鍵簇小於這個值的時候,會在執行resize方法的時候變回樸素桶。

267-272行 常量定義:桶在可能轉變爲樹桶的情況下,最小的容量=64
解析:這個值必須是(32)的倍數,樹桶會佔用更大的空間,如果小於這個值,會使resize方法和“樹化”方法衝突。

274-開始 Node類定義:用於樸素鍵值對的類
  1. hashCode()方法:將key的哈希碼與value的哈希碼做異或運算得到。
  2. setValue(V newValue):返回修改前的(舊的)Value值
解析:和大多數類一樣 沒有太特別的方法。

320-339行 終態靜態方法:int hash(Object key)
解析:將key的hash值分爲高位(前16位)和低位,將高位/低位做XOR(異或)運算。減少碰撞的可能性(比如Float型的哈希碼就可能是連續的整數)

341-362行 靜態方法:static Class<?> comparableClassFor(Object x) {}
解析:實現156-172行的效果。通過反射拿到參數的繼承接口/類名,如果實現了Compareable接口,則返回參數的類型,否則返回Null.

364-372行 終態靜態方法:static int compareComparables(Class<?> kc, Object k, Object x) 
解析:實現156-172行提及的效果。對兩個類型的參數進行比較(已經檢查過K的類型爲實現過Comparable接口)。
返回:如果X匹配參數KC的類型,返回compareTo方法的結果,否則返回0.

377-385行 終態靜態方法:static final int tableSizeFor(int cap) 
解析:對參數進行運算,將參數*2,如果>=規定的最大容量(237-242),將返回最大容量值,而不是*2後的值
返回值:2*cap,或者是MAXIMUM_CAPACITY(1<<30)

415行 字段:transient int modCount;
解析:每個集合類都有這麼一個字段屬性。用於記錄這個集合更新的次數(比如map中鍵值對的數量發生改變,或者一些內部變化,比如rehash)。主要用於檢驗迭代器的快速失敗情況。

446行 構造方法: public HashMap(int initialCapacity, float loadFactor)
解析:提供兩個參數,分別用於賦值初始容量和裝載因子。對兩個值進行常規的檢查,如果初始容量大於MAXIMUM_CAPACITY,則賦值爲MAXIMUM_CAPACITY。裝載因子不可爲非數字或負數、0


487行 構造方法:public HashMap(Map<? extends K, ? extends V> m)
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }


解析:將Map類型的參數內的所有鍵值對信息轉換爲Hashmap類型的,使用默認的裝載因子0.75,並實現了Map.putAll()方法。
如果接收的對象已經被初始化了,那麼直接判定
是否大於門檻值(當前容量*裝載因子),大於則resize()擴容;
如果沒被初始化,那麼table數組是爲null,這時候獲取當前需要轉換map的能容納的最小容量t(m.size()/0.75+1),如果t>hashmap的門檻值threshold,那麼將hashmap的門檻值置爲map的兩倍。因爲如果threshold小於t的話,在putVal()的過程中會需要馬上調用resize()方法,造成不必要的性能損失。
在完成以後,實現map的Put方法(putVal()),將鍵值對放入對應的hashmap實例中。

554行 實例方法:public V get(Object key)
 public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
解析:即普通的獲取map中的鍵值對映射的值,不過注意,因爲hashmap允許鍵、值爲null,所以get方法返回Null並不能作爲不存在相對應鍵的判定,而是應該採用containsKey()方法來達到這個效果。

566行 終態方法:final Node<K,V> getNode(int hash, Object key) 
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 {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
解析:該方法是HashMap的核心方法之一。
table是HashMap採用拉鍊法的數組,裏面存放一條鏈表,鏈表內都是hash值相同,造成碰撞的元素。
first是對應的key值對應的hash值,在table數組中對應hash值位置鏈表的第一個元素。如果要找的key就是這個first,則將該first鍵值對返回。
否則將first指向下一個鏈表元素節點,注意這時候可能鏈表並不是單鏈表,而是二叉鏈表即紅黑樹情況,所以要檢查一下是否節點是TreeNode的實例。
如果是,則採用TreeNode的getTreeNode方法獲取相應的鍵值對並返回。
否則,檢查當前節點e的hash值,以及元素是否與查找本身==,如果相等,則返回當前節點鍵值對e,否則繼續遍歷鏈表。
經歷過以上步驟如果仍沒有返回,則沒找到對應的鍵值對,返回Null。

594行 實例方法:
public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }
解析:本方法採用566行實例方法,這也是爲什麼containsKey能判斷鍵是否存在。因爲getNode不會返回Null,除非查找的元素不存在。

624行 終態方法 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean exvict)
 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        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;
            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);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            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;
    }
解析:這是HashMap中最重要的方法之一。接收四個參數,其中後兩個布爾型,一個代表在所對應鍵的值已經存在的情況下,是否執行覆蓋;一個代表hashMap的table字段是否處於創建狀態(前面提到,table僅在初始化時執行一次(不是終態是因爲包括resize操作))。而常用的Put()方法,調用時是採取覆蓋操作,不創建table.
(1)和getNode方法類似,首先根據key的哈希碼找到對應拉鍊數組的索引。
(2)如果對應的桶是紅黑樹桶,那麼執行樹節點的PutTreeVal,這裏暫時不做介紹;否則在本方法內執行Put操作。
(3)定義一個int型bitCount,用於計算鏈表的長度,當鏈表長度達到treefy的門檻值(8-1=7,因爲是執行Put操作後就會變成8,而前面已經提到過,樹化操作會在put/remove方法結束後執行)時,桶將從樸素桶轉變爲紅黑樹桶(執行treeifyBin(tab, hash)操作)
(4)對樸素桶進行單鏈表的順序查找,如果找到則跳出循環。
(5)跳出循環後進行判斷,e(即查找指針p的next)是否爲空。我們知道,這個for循環只有兩種方式跳出,一種是e=p.next==null,即鏈表中沒有找到所查找的鍵;另外一種是e==key,即找到了被查找的鍵。所以執行分支語句,如果本身就存在這個鍵,那麼將新值替換掉舊值,同時將舊值返回(標紅處的語句實現了這個操作)
(6)這個時候函數的主要功能put已經完成,接下來是一些hashmap本身屬性的操作。比如++modcount,同步迭代器屬性。對size進行判斷,是否需要resize()操作,這些都是在插入操作(沒有在上面返回證明鍵值對是插入的,而不是更新的)完成後進行的,因爲改變hashmap本身的屬性。
(7)後面執行一次LinkedHashMap繼承的afterNodeInsertion方法,不過不會執行,因爲傳入的是false,該方法可能在插入後執行刪除頭結點的操作。1

676行 終態方法: final Node<K,V>[] 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;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                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) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        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"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        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)
                        newTab[e.hash & (newCap - 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;
			//留在原數組索引位置
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
			//挪到新位置,原索引+oldCap(n)
                            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;
    }
解析:這是HashMap中核心方法之一,用於拓展map中鍵值對數組的大小(拉鍊法的數組)。如果還沒有被初始化,那麼table數組會被初始化爲默認大小(16),否則會拓展爲原大小的兩倍。鏈表中的元素,因爲是產生“碰撞”而進去的,所以會有相同的table索引值(table[(table.length-1)&hash]);或者會分散在偏移量爲2的乘方的新的table數組中
(1)如果擴展前的長度oldCap爲正數,則對它進行判斷:如果已經到了hashmap數組最大長度Max_Value,那麼將門檻值threshold置爲Int型最大數,避免下次擴展操作,並返回原數組;如果oldCap小於最大長度值,則將oldCap擴大一倍並賦值給newCap。newCap<=最大長度且oldCap>=默認值16時,設定新門檻值newThr爲舊門檻值的一倍;如果其中一個不滿足,newThr仍舊爲0。
(2)else if (oldThr > 0) 此時已經出了 oldCap>0的條件,所以我們知道,這個if判斷還有個隱藏條件是oldCap==0,因爲如果oldCap小於0,早在構造函數的時候已經拋出了異常。可以改寫爲(oldCap==0&&oldThr>0) newCap=oldThr.  將舊門檻值賦給了新數組長度
(3)else 此時的條件是:oldCap==0且oldThr==0  這時將默認容量和默認裝載因子賦給新的
(4)if (newThr == 0)  爲什麼newThr會是0呢。回到(1)中,如果舊容量oldCap擴容兩倍後大於等於最大長度或舊門檻值oldThr<16,都會使newThr沒被賦值,還是初始的默認值。所以這會兒就要進行判斷了,到底是哪個原因。後面計算了新的門檻值,滿足條件(newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY )則新門檻值newThr=newCap*loadFactor,否則也置爲Int型最大值。
誒,爲什麼這裏只判斷了其中一個條件呢?筆者猜測是這個原因:如果oldCap<默認值16,那麼可以知道必然是調用了有參數的構造方法指定了初始容量大小,而有參構造方法(int initCap,float factor)裏面有一行(456行) threshold=tableSizefor(initCap),這個方法返回的是傳入參數(即初始容量)的兩倍。我們知道,默認裝載因子不過0.75而已,即便容量翻一倍,也就1.5倍(原容量)大小的門檻值,而這裏設定的已經是2倍了,newThr(1.5*oldCap)<oldThr(2*oldCap)自然不用管它。到這裏resize方法關於hashmap本身屬性值改變就完成。
(5)這時就要開始對原數組的數據“搬家”到新數組了,由於指針膨脹的原因,所以要重新計算一次原數組中數據對應到新數組中hash的索引。
(6)
if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
吶,可以看到,如果鏈表裏面只有一個節點(即沒有發生碰撞),那肯定是樸素桶了,直接重新計算索引值搬家即可;否則,發生了碰撞,判斷當前節點是不是樹節點,如果是,執行樹節點的分散方法。(注:本方法只會被resize方法調用)
(7)720行:  if ((e.hash & oldCap) == 0) 
體現智商的時候來了,很明顯- - 筆者的智商不是特別夠,還好有很多大佬珠玉在前,這篇博文這行介紹的非常好:點擊打開鏈接 同時,這個地方也是1.8與1.7差別最大的地方之一,hashmap省去了1.7的rehash操作,提高了性能,同時也降低了碰撞的可能性。
筆者就簡單的在這裏講一講他的思想,這個方法的目的是爲了避免重新計算hash值
我們知道,hashmap擴充是2的乘方,而求索引是table[hash&(n-1)],其中n是數組的長度,hash是鍵的哈希碼。我們知道,hash和n都是int型的,32位長度。而n變成了2n,實際上就是高一位的值變了,可能是0,也可能是1。那麼(hash&n-1)的值實際上就是要麼沒變(還是原索引);要麼變了(原索引+oldCap)。
爲什麼是e.hash&oldCap(其實就是n,而不是n-1了)。根據上面的話我們知道,實際上就是判斷新增加的高位到底是0,還是1.如果是0,則留在原位置不動;如果是1,則需要挪到+oldCap處。而hashmap的Capacity一直保持爲2的冪次,所以可以理解爲n比n-1向前進了一位,即只針對n處,判斷高位是0還是1,而分散到相應的數組位置。  如果還沒懂的同學可以再看看這一篇文章:https://www.zhihu.com/question/28365219

這也是爲什麼前面註釋會提到說鏈表中的元素會被均勻的分散到偏移量爲2的乘方的新數組中,因爲要麼是留在元數組中,要麼是原索引+oldCap(擴大了oldCap的一倍,即*2)。同時我們可以近似的看做,e.hash相對高位的0、1表現是隨機的,這樣就更加減小了碰撞的可能性,散列的更加均勻。
(8)
然後根據前面的這個判斷,將單鏈表中元素分爲了兩串lo(不需要移動的)、hi(需要移動的)。
定義了兩個變量head/tail,Head是串單鏈表的頭部,tail是串的尾部,用tail將鏈表串接起來。
用尾插法將 會處於新數組中的元素用尾插法串聯起來,當達到末尾的時候,執行操作,將單鏈表放入新數組相應的索引位置。整個resize()方法結束。
(9)與JDK 1.7的區別:
  1. 1.8中沒有rehash獲得新的hash值再將newHash&(newCap-1),而是在原基礎上利用原hash/容量,將鏈表中元素分成了要遷移和不遷移的兩部分
  2. 1.8中resize方法保全了鏈表中原順序的有序性,1.7則將原鏈表順序倒置
754行:終態方法:final void treeifyBin(Node<K,V>[] tab, int hash) 
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                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)
                hd.treeify(tab);
        }
    }
解析:本方法是爲了將樸素桶內的單鏈表節點全部替換成樹節點,如果表數組長度太小(比如小於轉換閾值64),則會調整大小。這個操作會在鏈表長度>=樹節點轉換閾值8的時候觸發(643行)
(1) if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)  如果傳入的數組爲空,或者數組的長度<64,就會執行resize方法。因爲根據前面的解讀我們知道,樹節點相對樸素桶更佔空間,因此會設定一個表數組長度的最小值,避免太高的碰撞。
(2)將數組中鏈表的元素用尾插法插入進去
(3)數組鏈表的頭結點不爲空,則對鏈表節點進行紅黑樹平衡操作,包括左右旋轉,使其平衡。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章