jdk8底層源碼閱讀 HashMap源碼解析

目前使用的是jdk1.8版本,也是國內主流版本,不同版本之間實現可能不盡相同但理念類似,也可以佐證

Hash Map 解析

HashMap是基於哈希表的Map接口的實現類,此實現提供所有可選的映射操作,並且允許null值和null鍵。

HashMap類大致相當於哈希表,不同步,允許空值,不保證放入順序,提供基本的操作穩定的性能(get和put),fail-fast迭代

HashMap中的一個實例具有影響其性能的兩個參數: 初始容量和負載因子。
作爲一般規則,默認加載因子(.75)在時間和空間成本之間的良好平衡。
容量是在哈希表數組中數組元素的數量,和初始容量是簡單地在創建哈希表中的時間的能力。
負載因子是哈希表是如何充分允許獲得之前它的容量自動增加的措施。
當在哈希表數組中的數量超過了負載因子和容量的乘積(閾值),哈希表被重新散列 (即,rehash,內部數據結構被重建)
較高的值減少空間開銷,但增加了查找成本(反映在大多數HashMap類的操作,包括get和put的)。

同時,通過分析源碼,可以知道HashMap的結構是使用hash算法計算hash定位hash表[數組]下標,如果hash下標相同,則是hash碰撞,使用鏈表存儲相同hash的KV,
如果鏈表大於8則樹化,採用紅黑樹存儲以獲得更高讀寫性能,如果刪除元素紅黑樹節點少於等於6則退化成鏈表 .
在Java的數據結構中,數組是查詢快擴容慢,而鏈表是查詢慢擴容快,如果hash碰撞嚴重,那麼可能導致多鏈表多樹導致查詢慢

那麼提出一個假設,如果大量發生hash碰撞性能一定會慢嗎?下面按功能分類來深入源碼研究一下

重要計算方式

hash數組個數始終爲2的冪次,這一點在構造函數以及resize中可以得到驗證

  • hash & size
  • hash & ( size - 1 )
  • tableSizeFor

	/****
	 * 測試下標索引 
           
	 * 	hash & length
                00010 & 10000  -> 00000 
                10101 & 10000  -> 10000
        因爲長度爲2的冪次 , 因此 直接與  得到的結果永遠是 length 或者 0 
	 * 	hash & (length -1 )
        實際上是 hash 對長度的取模運算 ,這樣不管hash 計算多大 都不會出現數組越界情況
	 */
	public void testIndex(){
			int length = 16 ;
			IntStream.range(0,100).forEach(
					hash->{
						System.out.println(hash+"- &   ->"+(hash&length));
						System.out.println(hash+"- &-1 ->"+(hash&(length-1)));
						System.out.println();
					}
			);
	}


重要內部變量及結構

閾值 , 容量 , 底層數組 等



  /**
     * The default initial capacity - MUST be a power of two.
        默認容量
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 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.
        樹化閾值 , 在大於8個鏈表會樹化(紅黑樹)
     */
    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;
 

 /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
        底層 實際儲存數組
     */
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
        內部的 kv set 
     */
    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).
         fail-fast.  模式 記錄了關於hashmap的修改次數 
        同樣在 ArrayList 也存在 
     */
    transient int modCount;

    /**
     * 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;



    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    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;
        }
    }



    /**
     * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
     * extends Node) so can be used as extension of either regular or
     * linked node.
     */
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
 
        // ..... 篇幅有限,僅分析重點,省略多個方法 , 有興趣 可以 一一研究

    }
    /**
            LinkedHashMap.Entry<K,V>  實際上也是繼承了HashMap.Node 
            因此 在構建紅黑樹結構外 還保留了 雙向鏈表結構
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

    

構造函數解析

構造函數生成實例對象,是研究源碼的起點,構造函數有以下四個:

  • public HashMap()
  • public HashMap(int initialCapacity)
  • public HashMap(int initialCapacity, float loadFactor)
  • public HashMap(Map<? extends K, ? extends V> m)

最常用是無參構造 ,使用參數構造函數可以指定容量和負載因子(與擴容閾值相關) , 最後一種則是通過 Map 子類創建HashMap

 

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        // 無參構造 只是設置了負載因子 默認是0.75 
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    
    public HashMap(int initialCapacity) {
        // 調用的是兩個參數的構造函數
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

  /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) {
        // 帶參數構造函數 , 首先對參數必要的校驗 不能出現過大過小的值
        // 其實就是容量範圍 要在[0,MAXIMUM_CAPACITY]之間
        // MAXIMUM_CAPACITY = 2^29 = 1<<30
        // 還有一個出現的數值是 `Integer.MAX_VALUE` = 0x7fffffff 實際數值 是 (2^31)-1 =  1<<32-1
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        // 同樣 負載因子 也不能小於等於0
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
        // 賦值給負載因子後 調用了關鍵的函數 tableSizeFor 
        // 這個函數也是後面經常出現的一個函數
        // 首先,返回的是2的冪次的值,其次,這個值大於等於參數,總結就是 `大於等於參數的最小2次冪值`
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    // 大於參數cap的最小2次冪值
    // 這裏拿 13舉例 ,cap = 13
	static final int tableSizeFor(int cap) {
        // n = 13-1 = 12 
		int n = cap - 1;
        // 首先應該認識 >>> 運算,無符號右移,相當於除2
        // | 運算,或運算,有真則真,同假爲假,在二進制中的或運算則是逐位或, 1100 | 0110  =  1110 
        // n 二進制表示(前面的0,無意義所以省略了)  1100 = 12  
        // n>>>1 二進制表示 0110 = 6
        // 然後 |= 運算 ,與熟知的+=,-=,/= 運算類似 ,省略了計算賦值操作 ,相當於 n = n | n>>>1  
        // 即  n = 1100 | 0110 = 1110 14
		n |= n >>> 1;
        // n = 1110 | 0111  = 1111 15
		n |= n >>> 2;
        // n = 1111 | 0000 = 1111 15
		n |= n >>> 4;
        // n = 1111 | 0000 = 1111 15
		n |= n >>> 8;
        // n = 1111 | 0000 = 1111 15
		n |= n >>> 16;
        // 最後防止溢出 使範圍在[1,MAXIMUM_CAPACITY] 之間的冪次
        // 可以思考一下:可以看到後面幾步對於cap 13 而言屬於重複操作 , 那麼爲什麼還保留呢
        // java 使用unicode 編碼 一個字節8位 int 4字節 是32位 其中最高位 符號位
        // 在無符號右移的過程中
        // 先右移1位就保證最高位1位覆蓋1
        // 再右移2位就保證最高位1+2位覆蓋1
        // 再右移4位就保證最高位7位覆蓋1
        // 再右移8位就保證最高位15位覆蓋1
        // 再右移16位就保證最高位31位覆蓋1
        // 這也是設計的精妙之處,不管數據多大都能包含在內,目的是爲了覆蓋最高位之後的零,變爲1
        // 在覆蓋最高位之後, 最後的判斷限制了範圍,前面的移位保證了數值是最接近cap的值,並且做了+1 保證了冪次 
        // 同時,在開始就對cap進行了減1操作得到n , 目的就是爲了退位,防止出現當前cap爲2的冪次時取了更大的冪次
		return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
	}


    /**
     * Constructs a new <tt>HashMap</tt> with the same mappings as the
     * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
     * default load factor (0.75) and an initial capacity sufficient to
     * hold the mappings in the specified <tt>Map</tt>.
     *
     * @param   m the map whose mappings are to be placed in this map
     * @throws  NullPointerException if the specified map is null
     */
    public HashMap(Map<? extends K, ? extends V> m) {\
        // 負載因子
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        // 注入 map
        putMapEntries(m, false);
    }


賦值函數解析

賦值,保存鍵值對,常用put

  • public V put(K key, V value)
  • public V putIfAbsent(K key, V value)
  • public void putAll(Map<? extends K, ? extends V> m)

/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) {
        // 計算key 對象hash 
        // 通過 對象自己hash碼與 hash碼 無符號右移 16 位 取異或 得到hashMap 中數組的 hash 也是作爲數組的下標
        // 再次計算hash值 並且使用高16位與低16位異或也是爲了使 hash值更分散 , | 和 & 都會讓結果更偏向0,1而不是均勻
        // 使用位操作則是爲了性能更高 , 因爲平常的四則運算 使用10進制會有個轉換過程 而直接使用位運算則
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    /**
        關鍵性函數 實現了map 的 put 和 replace 方法 
        replace 方法 
    
     * Implements Map.put and related methods.
     *
     * @param hash hash for key 
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value 如果爲真 則不改變已經存在的值
     * @param evict if false, the table is in creation mode. 在linkedhashmap中使用 這裏是個空實現
     * @return previous value, or null if none 遇到相同的key 返回之前的值 如果不存在則返回null
     */
    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 = table數組    n 爲 table 長度 
        // 如果爲空最後使用resize 調整大小 , 然後賦值給n實際長度
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 初始化完成後 使用 `(n - 1) & hash` 定位下標 並且把下標賦值給 i 
        // 判斷 下標是否有值 沒有值則直接創建賦值 新節點 
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
        // 下標 有值 則 hash碰撞,hash碰撞不可避免,那麼發生hash碰撞
        // 解決辦法則是在發生hash碰撞時在節點上創建鏈表,如果數量到達一定程度,那麼可以預料,查詢性能也會下降
        // 因此在jdk8中 有樹化閾值 超過閾值則樹化,採用的是紅黑樹,性能方面有大大提升,性能爲O(log2(x)) 相比鏈表的O(x) 有大幅提升 
            
            Node<K,V> e; K k;
            // 判斷是否是相同的hash值 並且 判斷是否相同key則替換
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果是樹化節點則調用樹化方法putTreeVal
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 如果只是鏈表節點 , 則遍歷鏈表
                for (int binCount = 0; ; ++binCount) {
                    // 獲取下個節點賦值給e,如果爲空,則說明到了鏈表尾
                    if ((e = p.next) == null) {
                        // 鏈表表尾追加新節點
                        p.next = newNode(hash, key, value, null);
                        // 如果 大於等於 8-1=7,binCount從0開始,但是鏈表在next纔會計算,則相當於大於8即樹化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        // 跳出循環
                        break;
                    }
                    // 如果hash 相同 並且 key相等 直接跳出循環
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 下個節點接着循環遍歷
                    p = e;
                }
            }
            // e不爲空 表示有節點 所以判斷是否需要返回值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                // 如果 onlyIfAbsent fasle 或者 是 原節點爲空 則賦值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                // 空回調
                afterNodeAccess(e);
                // 返回原值
                return oldValue;
            }
        }
        // 增加修改標識
        ++modCount;
        // 如果超過閾值 重新調整大小
        if (++size > threshold)
            resize();
        // 空回調
        afterNodeInsertion(evict);
        return null;
    }


    /**
        實現了 對Map集合的全部 put 
     * Implements Map.putAll and Map constructor.
     *
     * @param m the map
     * @param evict false when initially constructing this map, else
     * true (relayed to method afterNodeInsertion).
     */
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        // 獲得集合的size
        int s = m.size();
        if (s > 0) {
            // 同樣 首先初始化判斷 初始化
            if (table == null) { // pre-size
                // 由size / 負載因子 得到大致 擴容容量 防止容量過小 導致多次擴容
                float ft = ((float)s / loadFactor) + 1.0F;
                // 如果 容量大於最大則爲最大容量
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                // 如果 容量大於 閾值 則重新計算閾值
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            // 如果 不爲空 並且s 大於閾值 直接重新調整
            else if (s > threshold)
                resize();
            // 預算大小初始化完成後 循環遍歷m的kv,調用putVal注入            
            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);
            }
        }
    }

擴容函數解析

將初始化 底層 table , 如果是空,則提供默認的初始化容量和擴容閾值
否則 則提供一個2的冪次的擴容 元素保持相同的索引或者創建一個新的table並移動2的冪次

擴容函數調用關係

  • final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict)

在m元素個數大於 threshold

  • putVal(int hash, K key, V value, boolean onlyIfAbsent,
    boolean evict)

table 爲空 或者 元素個數 大於 threshold

  • final void treeifyBin(Node<K,V>[] tab, int hash)

參數 tab 爲空 或者 小於 MIN_TREEIFY_CAPACITY

  • public V computeIfAbsent(K key,
    Function<? super K, ? extends V> mappingFunction)

  • public V compute(K key,
    BiFunction<? super K, ? super V, ? extends V> remappingFunction)

  • public V merge(K key, V value,
    BiFunction<? super V, ? super V, ? extends V> remappingFunction)



/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 *
 * @return the table
 */
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) {
        // 容量大等 int 容量 2^32-1 閾值賦值爲int最大值 並返回容量
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        } // 1.這裏首先新容量 = 當前容量左移1位 即 oldCap*2
          // 2.也不能大於最大容量
          // 3.當前容量大於默認容量16
          // 4.新閾值 = 當前閾值左移1位 即乘2
          // 思考一下:這裏沒有else 兜底 是否存在 `左移之後大於最大容量` `左移之後判斷當前容量小於默認容量` 導致與預想操作不符合的情況
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // 閾值大於零且容量不大於0,作爲新的容量
        // 因爲有提供指定容量和閾值的構造函數,因此可能存在這種情況
        newCap = oldThr;
    else {               
        // 如果 閾值容量都不大於0 則未初始化 提供了默認的初始化值
        // 這裏是 DEFAULT_INITIAL_CAPACITY 1 << 4 16 所以默認容量是16
        // 這裏是 0.75*16 12 所以 閾值是 12 
        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);
    }
    // 閾值與底層table 賦值給容器
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 原容器有值 那必須把值賦值給新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)
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果是樹節點 調用 split 增加節點
                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;
                        // 這裏判斷 `e.hash & oldCap ` 與前面的計算下標索引`e.hash & (newCap - 1)` 不一樣,得到的結果始終是oldCap 或者 0 
                        // 爲什麼這樣計算? 
                        //  這樣就把 一條hash碰撞產生的數量多的鏈表 一分爲二 
                        // 那麼 hash 越分散 查詢性能越高 , 其實也就是rehash的一步
                        if ((e.hash & oldCap) == 0) { // 爲0
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else { // 不爲 0 
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                        // 沒有子節點終止
                    } while ((e = next) != null);
                    // 將上面兩條鏈表接入,同時低位不動,高位偏移原始容量
                    // 思考一下:直接偏移不會數組越界嗎?直接偏移後面如果使用`hash & (newCap -1)` 還能定位到高位嗎?
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

樹化與反樹化函數解析

在達到樹化閾值時,會使用樹化操作從鏈表到樹,而達到反樹化閾值時會反樹化成爲鏈表,在1.8中使用的是紅黑樹,而紅黑樹查詢性能能夠有O(log2(n)),極大彌補了鏈表的查詢慢問題

樹化函數



   /**
      替換所有鏈接的節點在指數給定的哈希表,除非是太小了,在這種情況下,調整大小來代替
      參數傳遞的也是 底層的tab 哈希表 和 對應定位的hash值
    * Replaces all linked nodes in bin at index for given hash unless
    * table is too small, in which case resizes instead.
    */
   final void treeifyBin(Node<K,V>[] tab, int hash) {
       int n, index; Node<K,V> e;
       // 判斷非空擴容,以及判斷最小樹化容量 來調整hash表容量
       // 也就是如果容量小而導致的樹化,應該重新調整大小來rehash
       // 思考一下:既然樹化,那麼裏面不應該是有值嗎,這裏判斷是否爲空是否多此一舉?
       if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
           resize();
       else if ((e = tab[index = (n - 1) & hash]) != null) {
           // 如果對應元素不爲空,開始樹化
           // hd : head 頭  ,  tl:tail 尾
           TreeNode<K,V> hd = null, tl = null;
           do {
               // 轉換Node->TreeNode
               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);
       }
   }


       /**
           TreeNode 的實例方法  從這個節點開始構建樹
        * Forms tree of the nodes linked from this node.
        */
       final void treeify(Node<K,V>[] tab) {
           TreeNode<K,V> root = null;
           // 此處的this 是 TreeNode 實例 不是HashMap , 同時 此時 也還是鏈表結構 未構成樹
           // 開始遍歷鏈表 每次都是使用 x 作爲當前操作節點
           for (TreeNode<K,V> x = this, next; x != null; x = next) {
               // 獲得下一節點
               next = (TreeNode<K,V>)x.next;
               // 將樹左右節點 置空
               x.left = x.right = null;
               // 第一個結點賦值根節點 , 後面有自平衡操作,所以不用擔心會傾斜
               if (root == null) {
                   x.parent = null;
                   x.red = false;
                   root = x;
               }
               else { // 第二個開始
                   K k = x.key;
                   int h = x.hash;
                   Class<?> kc = null;
                   // 然後從根節點開始遍歷比較,因爲採用的是二叉查找樹,所以需要比較大小,比較的是計算得來的hash值
                   for (TreeNode<K,V> p = root;;) {
                       int dir, ph;
                       K pk = p.key;
                       // h 是當前需要插入節點的hash 對比 p 遍歷樹節點hash
                       // h 小 往左遍歷 h 大 往右 
                       if ((ph = p.hash) > h)
                           dir = -1;
                       else if (ph < h)
                           dir = 1;
                       // hash 無法比較出大小 
                       // 判斷  當前插入的節點的key是否實現可比較接口 , 並且獲得了class對象
                       // 通過調用compareTo再次比較得出大小, 如果還是 0 
                       // 則進入最後的 tieBreakOrder 比較 , 而方法中使用了 System.identityHashCode 方法 比較Object實際hash 而不使用重寫後的hashCode方法
                       else if ((kc == null &&
                                 (kc = comparableClassFor(k)) == null) ||
                                (dir = compareComparables(kc, k, pk)) == 0)
                           dir = tieBreakOrder(k, pk);

                       TreeNode<K,V> xp = p;
                       // 根據上面的大小得出下一個遍歷左 , 右 ,如果不爲空接着遍歷
                       if ((p = (dir <= 0) ? p.left : p.right) == null) {
                           // 不爲空 則達到了葉子節點 , 將待插入節點x 接上 xp = p 
                           x.parent = xp;
                           // 判斷下接在左,還是右 
                           if (dir <= 0)
                               xp.left = x;
                           else
                               xp.right = x;
                           // 這一步 是自平衡 , 下面 詳細講解這個函數
                           root = balanceInsertion(root, x);
                           break;
                       }
                   }
               }
           }
           // 生成樹 完成 後 要確保 根節點是在hash表數組上
           moveRootToFront(tab, root);
       }


樹化函數之自平衡算法

待補充…

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