Java集合—HashMap的源碼深度解析與應用

  超過萬字的JDK1.8的HashMap主要方法源碼解析,深入至底層紅黑樹的源碼,並且與JDK1.7的HashMap做了比較全面的對比,最後給出了比較完整的HashMap的數據結構圖!

  本文主要是對JDK1.8的HashMap的主要方法實現做了分析,對於一些基礎的知識,認爲大家在看這篇文章之前是都懂得的,比如哈希表的原理、紅黑樹的原理!如果大家有不瞭解這些原理的一定要去看看相關文章,否則如果直接看下面的源碼的分析肯定有你看不懂的!
  數據結構—紅黑樹(RedBlackTree)的實現原理以及Java代碼的完全實現,這是看懂HashMap底層紅黑樹源碼必備的基礎知識!
  數據結構—散列表(哈希表)的原理以及Java代碼的實現,HashMap就是一張散列表,這是關於散列表的介紹!

1 HashMap的概述

public class HashMap<K,V>
  extends AbstractMap<K,V>
  implements Map<K,V>, Cloneable, Serializable

  HashMap,來自於JDK1.2的哈希表的實現,JDK1.8底層是使用數組+鏈表+紅黑樹來實現的(JDK1.7是使用數組+鏈表實現的),使用“鏈地址法”解決哈希衝突。
  實現了Map接口,存放的自然是key-value形式的數據,擁有Map接口的通用操作。允許null鍵和null值,元素無序。
  實現了Cloneable、Serializable標誌性接口,支持克隆、序列化操作。
  此實現不是同步的,可以使用Collections.synchronizedMap()方法獲得一個同步的Map。
  默認容量爲16,第一次存放元素時初始化;默認加載因子爲0.75;擴容增量爲增加原容量的1倍,即變成原容量的兩倍。

2 主要類屬性

  主要類屬性包括一些默認值常量屬性,還有一些關鍵屬性。
  從這些屬性可知,默認初始容量16,最大容量2^30,加載因子0.75。
  鏈表樹形化閾值8(大於),哈希表樹形化閾值64(大於等於),resize時樹還原閾值6(小於等於)。

/**
 * 默認初始容量爲16,所有的容量不許時2的冪次方,這在哈希算法中會使用到。
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

/**
 * 最大容量爲1 << 30,即2^30次方。
 * 我們知道int類型的範圍是[-2^31 ~ 2^31-1],因此這裏的2^30實際上就是int範圍類的最大的2的冪次方值。
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 默認加載因子爲0.75
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 鏈表樹形化閾值,即鏈表轉成紅黑樹的閾值,在存儲數據時,當鏈表長度 大於8 時,則將鏈表轉換成紅黑樹。
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 紅黑樹還原爲鏈表的閾值,當在擴容時,resize()方法的split()方法中使用到該字段
 * 在重新計算紅黑樹的節點存儲位置後,當拆分成的紅黑樹鏈表內節點數量 小於等於6 時,則將紅黑樹節點鏈表轉換成普通節點鏈表。
 * <p>
 * 該字段僅僅在split()方法中使用到,在真正的remove刪除節點的方法中時沒有用到的,實際上在remove方法中,
 * 判斷是否需要還原爲普通鏈表的個數不是固定爲6的,即有可能即使節點數量小於6個,也不會轉換爲鏈表,因此不能使用該變量!
 */
static final int UNTREEIFY_THRESHOLD = 6;


/**
 * 哈希表樹形化的最小容量閾值,即當哈希表中的容量  大於等於64 時,才允許樹形化鏈表,否則不進行樹形化,而是擴容。
 */
static final int MIN_TREEIFY_CAPACITY = 64;


/**
 * 底層存儲key-value數據的數組,長度必須是2的冪次方。由於HashMap使用"鏈地址法"解決哈希衝突,table中的一個節點是鏈表頭節點或者紅黑樹的根節點。節點類型爲Node類型,後面我們會分析到,Node的實際類型可能表示鏈表節點,也可能是紅黑樹節點。
 */
transient Node<K, V>[] table;


/**
 * 集合中鍵值對的數量,可通過size()方法獲取。
 */
transient int size;


/**
 * 擴容閾值(容量 x 加載因子),當哈希表的大小大於等於擴容閾值時,哈希表就會擴容。
 */
int threshold;

/**
 * 哈希表實際的加載因子。
 */
final float loadFactor;

3 主要內部類

  HashMap的內部類比較多,這裏講解主要的內部節點類,一個是Node節點,即普通鏈表節點;另一個是TreeNode節點類,即紅黑樹節點。
  實際上Node節點直接實現了Map.Entry(Map體系中的集合的內部節點實現類的超級接口),實現了Map.Entry接口的全部方法。而TreeNode直接繼承了LinkedHashMap.Entry節點類,而LinkedHashMap類中的節點類Entry則是繼承了Node節點類。
  雖然它們的關係有點繞,但是TreeNode和Node仍然屬於Map.Entry節點體系,並且TreeNode節點類通過LinkedHashMap.Entry間接繼承了Node節點類(爺孫關係)。因此底層數組table中的Node的實際類型可能就是鏈表節點Node,也可能是紅黑樹節點TreeNode。
在這裏插入圖片描述

3.1 Node

/**
 * JDK1.8的HashMap的鏈表節點實現類(JDK 1.7 使用Entry類,只是名字不一樣)。
 * <p>
 * 具有hash屬性,用於存放key的hashCode方法的返回值,避免重複計算。
 * 具有key、value屬性用於存放鍵值對。
 * 具有next屬性,由於HashMap使用"鏈地址法"解決哈希衝突,因此使用next指向後來加入的 存放在同一個桶位置(即哈希衝突)的節點。
 * <p>
 * 實現了Map.Entry(Map體系中的集合的內部節點實現類的超級接口)。
 * 實現了Map.Entry接口的全部方法,比如getKey、getValue、setValue,總之:比較簡單。
 */
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;
    }
}

3.2 TreeNode

/**
 * JDK1.8的HashMap的紅黑樹節點實現類,直接實現了LinkedHashMap.Entry節點類。
 * <p>
 * 具有傳統紅黑樹節點該有的屬性,比如兩個字節點、父節點、節點顏色等。
 * 具有鏈表樹化的方法和樹還原鏈表的方法,具有查找、存放、移除樹節點的方法,具有調整平衡、左旋、右旋的方法,總之:比較複雜。
 * 後面會具體分析它的源碼!
 */
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;
    //節點的顏色,默認是紅色
    boolean red;

    /**
     * 構造方法,實際上只是調用了父類LinkedHashMap.Entry的構造方法,
     * 而在LinkedHashMap.Entry的對應構造方法中,又調用父類Node的構造方法
     *
     * @param hash key的hashCode返回值
     * @param key  k
     * @param val  v
     * @param next 鏈表的下一個節點引用
     */
    TreeNode(int hash, K key, V val, Node<K, V> next) {
        super(hash, key, val, next);
    }

    /**
     * 返回包含此節點的樹的根節點
     */
    final TreeNode<K, V> root() {
        //……
    }

    /**
     * 把給定的節點是指爲桶的第一個節點,即數組的節點
     */
    static <K, V> void moveRootToFront(Node<K, V>[] tab, TreeNode<K, V> root) {
        //……
    }

    /**
     * 從當前節點開始通過給定的hash和key查找節點
     */
    final TreeNode<K, V> find(int h, Object k, Class<?> kc) {
        //……
    }

    /**
     * 從根節點開始通過給定的hash和key查找節點
     */
    final TreeNode<K, V> getTreeNode(int h, Object k) {
        //……
    }

    /**
     * 比較節點大小,用來排序
     */
    static int tieBreakOrder(Object a, Object b) {
        //……
    }

    /**
     * 鏈表樹化
     */
    final void treeify(Node<K, V>[] tab) {
        //……
    }

    /**
     * 紅黑樹還原爲鏈表,removeTreeNode、split方法中會調用到
     */
    final Node<K, V> untreeify(HashMap<K, V> map) {
        //……
    }

    /**
     * 存放樹節點
     */
    final TreeNode<K, V> putTreeVal(HashMap<K, V> map, Node<K, V>[] tab,
                                            int h, K k, V v) {
        //……
    }

    /**
     * 刪除樹節點
     */
    final void removeTreeNode(HashMap<K, V> map, Node<K, V>[] tab,
                              boolean movable) {
        //……
    }


    /**
     * resize時,對紅黑樹節點的調用方法,包含了untreeify的邏輯
     */
    final void split(HashMap<K, V> map, Node<K, V>[] tab, int index, int bit) {
        //……
    }

    /**
     * 左旋
     */
    static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root,
                                                    TreeNode<K, V> p) {
        //……
    }

    /**
     * 右旋
     */
    static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root,
                                                     TreeNode<K, V> p) {
        //……
    }

    /**
     * 插入後調整平衡
     */
    static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root,
                                                          TreeNode<K, V> x) {
        //……
    }

    /**
     * 刪除後調整平衡
     */
    static <K, V> TreeNode<K, V> balanceDeletion(TreeNode<K, V> root,
                                                 TreeNode<K, V> x) {
        //……
    }

    /**
     * 遞歸不變性檢查
     */
    static <K,V> boolean checkInvariants(TreeNode<K,V> t) {
        //……
    }
}

4 構造器

4.1 HashMap()

public HashMap()

  構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。

/**
 * 默認構造函數,可以看到並沒有進行底層數組初始化,只是設置了加載因子爲默認加載因子,即0.75
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}

4.2 HashMap(initialCapacity)

public HashMap(int initialCapacity)

  構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。如果初始容量爲負,則拋出IllegalArgumentException異常。

public HashMap(int initialCapacity) {
    //內部調用指定容量大小和默認加載因子0.75的構造函數
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

4.3 HashMap(initialCapacity, loadFactor)

public HashMap(int initialCapacity,float loadFactor)

  構造一個帶指定初始容量和加載因子的空 HashMap。如果初始容量爲負或者加載因子爲非正,則拋出IllegalArgumentException異常。
  注意,加載因子可以大於1。

/**
 * 構造一個帶指定初始容量和加載因子的空 HashMap。
 * 如果初始容量爲負或者加載因子爲非正,則拋出IllegalArgumentException異常。
 */
public HashMap(int initialCapacity, float loadFactor) {
    /*1 參數檢測*/
    // 如果指定的初始容量小於0,那麼拋出IllegalArgumentException異常
    // 指定初始容量必須非負數,否則報錯
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);

    // 如果指定的初始容量如果大於最大容量,那麼初始容量等於最大容量
    // 即HashMap的最大容量只能是MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;

    // 如果加載因子小於等於0或者不是一個數值類型,那麼拋出IllegalArgumentException異常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                loadFactor);

    /*2 初始化參數*/
    /*到這一步,說明上面的檢測全部通過*/
    // 設置實際加載因子
    this.loadFactor = loadFactor;

    // 設置“擴容閾值”。注意這裏設置的值並不是真正的閾值,因爲我們說過容量只能是2的冪次方,因此需要先確保容量是2的冪次方纔行
    // 這裏的tableSizeFor方法僅僅只是將傳入的容量轉化爲大於等於該容量的最小2的冪次方,然後賦值給threshold這個變量臨時存儲真正的初始容量,而真正的閾值在後面還會重新計算
    /*2.1 設置真正的初始容量*/
    this.threshold = tableSizeFor(initialCapacity);
}


/**
 * 2.1 tableSizeFor(initialCapacity),類似於JDK 1.7 中 inflateTable()裏的 roundUpToPowerOf2(toSize),或者類似於JDK1.8 ArrayDeque中的allocateElements(numElements)方法
 * 該方法用於將傳入的容量轉化爲大於等於該容量的最小2的冪次方值,即用於計算真正的初始化容量
 * 該算法首先讓指定容量cap的二進制的最高位後面的數全部變成了1,
 */
static final int tableSizeFor(int cap) {
    // 使用cap-1來計算,是因爲下面的5行算法是 嘗試查找大於數n的最小2的冪次方-1,因此要想查找大於等於cap的最小2的冪次方,只能使用cap-1來進行運算
    int n = cap - 1;
    //使用無符號右移和位或操作,嘗試將n的最高位1的所有低位全部都變成1,即變成了一個奇數。
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    //上面的算法看起來很完美,但是實際上還是有兩個侷限性的:
    //1 如果cap=0,那麼經過上面的算法計算出來的n爲-1,小於了0
    //2 如果cap> (1<<30) ,那麼經過上面的算法計算出來的n爲2147483647,即int最大值,大於了最大容量
    //因此還需要下面的3個判斷:
    //1如果n小於0,即如果cap傳入0,那麼n=0-1=-1,那麼返回初始容量1
    //2否則,如果n大於等於最大容量,那麼就返回最大容量;
    //3否則,由於此時n爲(大於原n的最小2的冪次方-1),n+1之後正好是大於等於cap的最小2的冪次方,返回n+1。
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

  關於上面tableSizeFor的算法更詳細解釋,我在這篇文章的“ArrayDeque(int numElements)”部份有解釋:Java集合—ArrayDeque的源碼深度解析以及應用介紹。另外在JDK1.8的Integer類中的highestOneBit方法也使用了類似的算法,不過它是嘗試返回小於等於參數的2的冪次方!

4.4 HashMap(m)

public HashMap(Map<? extends K,? extends V> m)

  構造包含指定Map的新HashMap。所創建的HashMap具有默認加載因子(0.75)和足以容納指定 Map 中鍵值對的初始容量。如果指定的映射爲 null,則拋出NullPointerException異常。

/**
 * 構造包含指定Map的新HashMap。
 * 所創建的HashMap具有默認加載因子(0.75)和足以容納指定 Map 中鍵值對的初始容量。
 */
public HashMap(Map<? extends K, ? extends V> m) {
    //設置加載因子爲默認值0.75
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //1 將傳入的子Map中的全部元素逐個添加到HashMap中
    putMapEntries(m, false);
}


/**
 * 1 將傳入的子Map中的全部元素逐個添加到HashMap中
 * @param m 被加入的集合
 * @param evict 在構造器中調用該方法時傳入false,其他方法中(比如put、putAll)調用該方法時傳入true。實際上在HashMap中沒啥用,是留給其子類linkedHashMap用於實現LRU緩存的!
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //s爲m的實際元素個數
    int s = m.size();
    if (s > 0) {
        // 判斷table是否已經初始化
        if (table == null) { // pre-size
            // 未初始化,計算初始容量
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                    (int)ft : MAXIMUM_CAPACITY);
            // 計算得到的t大於閾值,則初始化閾值
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        // 已初始化,並且m元素個數大於閾值,進行擴容處理
        else if (s > threshold)
            resize();
        // 然後,將m中的所有元素循環添加至HashMap中
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            //該方法也是put方法內部調用的方法
            putVal(hash(key), key, value, false, evict);
        }
    }
}

5 put方法

public V put(K key,V value)

  將指定的鍵值對存入,此集合中。如果該集合以前包含了一個該鍵的映射關係,則舊值被替換。返回與 key 關聯的舊值;如果 key 沒有任何映射關係,則返回 null。(返回 null 還可能表示該映射之前將 null 與 key 關聯。)
  put方法是HashMap的核心方法之一,源碼較多,但是如果理解了put方法那麼其他方法也就比較簡單了!
  最頂層的put方法可以分爲兩步:

  1. hash(key)方法計算key的hash值;
  2. putVal方法存放k-v。

5.1 頂層put方法

/**
 * put方法開放給外部調用的API
 *
 * @param key   k
 * @param value v
 * @return 返回與key關聯的舊值;如果key沒有任何映射關係,則返回null。(返回 null 還可能表示該映射之前將 null 與 key 關聯。)
 */
public V put(K key, V value) {
    //內部調用putVal方法,可以看到第一個參數是hash(key)方法的返回值,因此真正的第一步實際上是調用hash(key)方法
    return putVal(hash(key), key, value, false, true);
}

5.1.1 hash方法計算hash值

  hash(key)方法用於計算key的hash值!

/**
 * JDK1.8獲取key的hash值:hashCode() + 1次位運算 + 1次異或運算(2次擾動)
 * 後面(1次位運算 + 1次異或運算)被稱作擾動算法,在原hashCode的值上進過了擾動算法,目的是用於降低哈希衝突概率
 * 相比於JDK1.7,擾動算法的代碼更簡單,但是原理不變
 *
 * @param key k
 * @return key的hash值
 */
static final int hash(Object key) {
    int h;
    //1次>>>運算 + 1次^運算(2次擾動);如果key==null則直接返回0
    //這裏我們能夠知道,對於key爲null的元素hash始終返回0,後面還會知道實際上key爲null的元素被始終固定存儲到數組0索引的位置
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

/**
 * JDK 1.7.0_79獲取key的hash值:hashCode() + 4次位運算 + 5次異或運算(9次擾動)
 * @param k k
 * @return key的hash值
 */
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    //4次位運算 + 5次異或運算(9次擾動)
    h ^= k.hashCode();
    //擾動算法
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

5.2 putVal方法

  putVal方法用於存放key-value,但實際上它的內部做了很多事,又可以分爲幾步:

  1. 判斷如果table數組爲null或者長度爲0,則初始化數組;
  2. 計算新節點存放位置i(哈希算法),並且判斷該位置是否發生哈希衝突;
  3. 如果沒有哈希衝突,創建新普通節點存放在該索引位置處;
  4. 如果有哈希衝突,設置變量e默認爲null,並進行下面的操作:
    a) 首先判斷table[i]的key是否與需插入的key相等,相等則e記錄該節點;
    b) 否則,判斷該位置的節點是否是紅黑樹節點類型,如果是那麼調用putTreeVal進行紅黑樹節點的添加或者替換。返回需要被替換的節點或者null(表示已添加),使用變量e記錄返回值;
    c) 否則,循環鏈表。查找是否有key與需插入的key相等,有則e記錄該節點,結束循環;否則在鏈表尾部插入新節點(尾插法),然後判斷鏈表長度是否大於8,如果是那麼將該鏈表樹形化,結束循環;
    d) 經過上面的三個判斷之一之後,統一判斷e是否不爲null,如果是,說明需要進行節點value的替換,則對節點e的value進行替換,並且返回舊的value,putVal方法結束
  5. 走到這一步,說明是新加了節點。那麼判斷如果++size大於擴容閾值threshold則調用resize方法進行擴容,putVal方法結束;否則不需要擴容,putVal方法結束
/**
 * 存放鍵值對的方法,HashMap的核心方法之一
 *
 * @param hash         key的hash值
 * @param key          需要插入的key
 * @param value        需要插入的值
 * @param onlyIfAbsent 如果爲true,並且傳入的key已經存在,並且舊的value不爲null,那麼不進行value替換,返回舊的value。如果不存在key,就添加key和value,返回null。在JDK1.8的新方法putIfAbsent、中,傳入true;
 *                     如果爲false,並且傳入的key已經存在,那麼進行value替換,並返回舊的value。如果不存在key,就添加key和value,返回null;
 * @param evict        在構造器中調用該方法時傳入false,其他方法中(比如put、putAll)調用該方法時傳入true。
 *                     實際上該參數在HashMap中沒啥用,是留給其子類linkedHashMap用於實現LRU緩存的!
 * @return 舊值,如果沒有則返回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;
    /*1 首先判斷如果table數組爲null或者長度爲0 則通過resize()初始化數組*/
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /*2 計算新元素應該存放的位置 通過(n - 1) & hash 計算得來,並且判斷該位置是否爲null,即判斷是否發生哈希衝突。
     * 2.1 如果爲null說明沒有哈希衝突,那麼調用newNode方法創建普通節點,存放在該索引位置處
     * */
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        /*2.2 如果不爲null,那麼說明這裏已經有元素了,發生了哈希衝突,開始解決衝突*/
        Node<K, V> e;
        K k;
        /*判斷 table[i]位置的元素的key是否與 需插入的key相等
         * 通過(hashcode的返回值hash)&&(==  || equals)來判斷,這說明要求  兩個key的hashCode相等  並且  兩個key==或者equals方法返回true 才能算是兩個key相等
         * */
        /*2.2.1 如果兩個key相等,那麼先用e展示保存p的引用,後續代碼會直接替換value*/
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
            /*2.2.2 如果兩個key不相等,那麼再判斷該數組索引的元素是否是紅黑樹節點類型,若是,則直接在樹中使用putTreeVal 插入 or 更新 鍵值對*/
        else if (p instanceof TreeNode)
            //若數組索引的元素是否是紅黑樹節點類型,則直接在樹中使用putTreeVal 插入 or 更新 鍵值對
            //如果是需要更新鍵值對,則返回需要更新的節點,如果是插入新節點,那麼返回null,使用e保存返回值
            e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
        else {
            /*2.2.3 否則,兩個key既不相等,也不是紅黑樹節點類型,那麼只能是鏈表類型
             * 遍歷table[i]處的鏈表,判斷Key是否已存在,同樣採用上面的方式判斷
             * 如果已存在,則結束遍歷,在後續代碼中 直接用新value 覆蓋 舊value
             * 如果遍歷完畢後,發現不存在,則直接在鏈表尾部插入數據(尾插法)
             * 新增節點後,需判斷鏈表長度是否>8,如果是,則把鏈表轉換爲紅黑樹
             * */
            /*循環鏈表*/
            for (int binCount = 0; ; ++binCount) {
                // 若e=p.next並且爲null,表示已到鏈表尾部,此時還沒有找到key值相同節點,則說明此聯表並不存在相同的key的節點
                // JDK 1.7的HashMap以及JDK1.8的Hashtable是從鏈表頭插入,即新節點永遠都是添加到數組的位置,原來數組位置的節點成爲新節點的next節點(頭插法)
                if ((e = p.next) == null) {
                    // 此時使用newNode新建鏈表節點,然後插入節點到鏈表尾部(尾插法)
                    p.next = newNode(hash, key, value, null);
                    //新增節點後,需判斷鏈表長度是否大於等於(8-1),如果是,則把鏈表轉換爲紅黑樹
                    //因此實際上是插入節點後如果鏈表長度大於8,則轉換爲紅黑樹,等於8的時候是不會樹化的!
                    //那麼有可能出現鏈表長度大於8但是還沒有樹形化的情況嗎?
                    //實際上是存在的,在remove某些節點之時,紅黑樹可能會轉換爲鏈表,此時節點數量可能會等於9,但此時屬於普通鏈表.在後面的remove方法中會講到!
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        //樹形化的方法,在該方法裏面還會判斷,即當哈希表中的容量大於等於64 時,才允許樹形化鏈表,否則不進行樹形化,而是擴容。
                        treeifyBin(tab, hash);
                    //結束循環
                    break;
                }
                //如果在鏈表中找到了key相同的節點,那麼直接結束遍歷,在後續代碼中 直接用新value 覆蓋 舊value
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //p保存e的引用
                p = e;
            }
        }
        //如果e不爲null,說明此時找到了key相同的節點,需要進行value的替換
        if (e != null) {
            V oldValue = e.value;
            //如果(onlyIfAbsent爲true,並且傳入的key已經存在,並且舊的value不爲null),那麼不進行替換,返回舊的value。
            if (!onlyIfAbsent || oldValue == null)
                //用新值替換舊值
                e.value = value;
            // 當一個節點被訪問時,會調用該方法,因此算作會調用方法。
            // 這個方法在HashMap是空實現,實際上是留給其子類linkedHashMap用來實現LRU緩存的。
            afterNodeAccess(e);
            //返回舊值,到此替換value的情況結束,方法結束
            return oldValue;
        }
    }
    /*走到這裏,說明是新建的節點*/
    // 此時結構改變次數自增1
    ++modCount;
    /*如果添加節點後size+1大於擴容閾值,開始調用resize方法擴容*/
    if (++size > threshold)
        resize();
    //插入元素之後會調用的回調方法
    //這個方法在HashMap同樣是是空實現,實際上是留給其子類linkedHashMap用來實現LRU緩存的。
    //在構造器中evict會傳遞false,其他方法中會傳遞true
    afterNodeInsertion(evict);
    //返回null
    return null;
}

/**
 * 創建普通節點
 */
Node<K, V> newNode(int hash, K key, V value, Node<K, V> next) {
    return new Node<>(hash, key, value, next);
}

下面對以上步驟的重點內容進行說明!

5.2.1 計算節點存儲位置的哈希算法

  新節點的最終位置是通過:(n - 1) & hash 計算出來的,其中的n表示數組的容量,hash是最開始計算出來的key的hash值。
  前面我們知道,對於key爲null的元素hash始終返回0,那麼實際上key爲null的元素被固定存儲到數組0索引的位置。因爲(n-1)&hash同樣始終返回0。
  這裏有幾個問題:

5.2.1.1 爲什麼不直接使用hashCode返回值最終存儲位置?

  因爲hashCode的大小事不固定的,容易超過數組索引的範圍。因此HashMap使用 (n-1) & hash 來確保最終的位置是出於索引範圍之內的。
  爲什麼(n-1) & hash 能確保最終的位置是出於索引範圍之內,可以看第三個問題!

5.2.1.2 爲什麼hash值的計算需要採用擾動算法,而不是直接用hashcode參與計算?

  因爲通常聲明Map集合時不會指定大小,並且Map集合的容量並不會很大,而一個1000000000000000,的二進制數的大小爲32768,實際上很少有這麼大容量的Map。所以在容量比較小的時候,比如容量n爲32,那麼n-1的二進制就是0001 1111,從第六位開始它的高位實際上全是0,我們知道0&(1或0)的結果都是0,因此實際上只有hashcode的低位能夠參與有效的&運算。
  我們知道上面的擾動算法是hashcode ^ (hashcode>>>16)。這裏的>>>是無符號右移運算符。我們將hashcode右移是16位,即將高16位移動到了低位16位,然後再與自身^運算。這樣最終的結果的高16位不變,但是低16位是原來的高16位和低16位進行 ^ 運算之後得出來的值,這樣實際上在最終計算存儲位置時,該hashcode的高位也參與了每一次的運算過程。
  因爲有可能兩個key的hashcode的低位是一樣的,但是高位不一樣,通過擾動算法之後,低位會變得不一樣,這樣計算出來的最終位置也是不一樣的,減少了碰撞率。

5.2.1.3. 爲什麼採用(n-1) & hash計算數組下標?爲什麼數組容量爲2的冪次方?

  上面講到,我們需要將最終計算出的下標固定在數組的索引範圍之內,此時我們想到的肯定有取模(求餘)算法,即hash % n,由小學數學可知:餘數一定會比除數小。此時可保證最終結果固定在[0,n-1]的範圍類,由於計算量不大,這也是很常用的一種哈希算法。
  而對於hash % n,就算n不是2的冪次方也能得出正確的範圍,那麼這裏爲什麼需要n(數組容量)一定是2的冪次方呢?
  實際上在hash % n的運算中,如果n爲2的冪次方。那麼hash % n等價於hash & (n-1)。而因爲&屬於位運算,位運算直接對內存數據進行操作,不需要像取模運算一樣轉成十進制,因此處理速度快,效率更高,當n是2的冪次方時,hash % n可以使用hash & (n-1)代替,這樣提高了哈希算法的效率!如果n不是2的冪次方,就不能轉換爲(n-1) & hash,雖然仍然能夠得到正確的結果,但效率卻沒有位運算的效率高!
  更深層次的原因,在hash % n的運算中,如果n爲奇數,那麼計算得到的餘數結果將會分佈的更加均勻(https://blog.csdn.net/lpf463061655/article/details/85130872)

5.2.2 數組初始化/擴容resize方法

  從源碼中,我們知道數組(哈希表)的初始化和擴容使用的是同一個方法:resize(),這個方法既支持初始化還支持擴容!
  resize方法同樣很複雜,可以大概分爲如下幾步:

  1. 如果老容量大於0,即已經初始化了哈希表,那麼可能需要擴容:
    a) 如果老容量oldCap大於等於最大容量,則不再擴容,直接返回舊數組,resize方法結束。
    b) 如果老容量小於最大容量,則需要擴容,計算擴容新容量newCap和新閾值newThr;
  2. 如果老容量等於0,即沒有初始化哈希表,計算初始化新容量newCap和新閾值newThr;
  3. 建立新數組newTab,容量爲新容量newCap,將table引用指向新數組。這個newCap可能是初始化容量也可能是擴容新容量;
  4. 如果舊數組oldTab有數據,那麼轉移舊數組的數據到新數組newTab;
  5. 返回新數組,resize方法結束。
/**
 * resize方法可用於:
 * 1 初始化哈希表;
 * 2 擴容;
 */
final Node<K, V>[] resize() {
    // 獲取舊數組
    Node<K, V>[] oldTab = table;
    //獲取數組的容量(長度),如果老的數組爲空,老的數組容量設爲0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //獲取老的擴容閾值
    int oldThr = threshold;
    int newCap, newThr = 0;

    /*1 如果老容量大於0,即已經初始化了哈希表 那麼需要擴容*/
    if (oldCap > 0) {
        /*1.1 如果老容量大於等於最大容量,則不再擴容*/
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 設置容量閾值爲int類型最大值,
            threshold = Integer.MAX_VALUE;
            //直接返回舊的數組,即不進行擴容。
            return oldTab;
        }
        /*1.2 如果老容量小於最大容量,則需要擴容*/
        // 首先計算新容量、新閾值
        // 新容量嘗試擴容爲老容量的2倍: newCap = oldCap << 1
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY) {
            // 如果新容量小於最大容量,並且老容量大於等於默認容量,那麼新閾值也嘗試擴容爲舊閾值的2倍:newThr = oldThr << 1
            newThr = oldThr << 1;
        }
    }
    /*2 否則,如果舊閾值大於0,即沒有初始化哈希表 那麼需要初始化*/
    /*這是 採用HashMap(int initialCapacity)、HashMap( initialCapacity, loadFactor)這兩個構造器創建Map對象,但是還沒有添加數據時,出現的情況。
     * 在那些構造器中,有這樣的設置threshold = tableSizeFor(initialCapacity); 即將實際初始容量值 賦給了擴容閾值,因此數組一定爲null,擴容閾值一定大於0。
     * */
    else if (oldThr > 0) {
        // 第一次初始化哈希表,新容量newCap就設置爲老的閾值
        // 新閾值newThr還是0,這裏沒計算,在後面會計算
        newCap = oldThr;
    }
    /*
     * 3 否則,那就是oldThr等於0,oldCap等於0。實際上就是採用 無參構造器 HashMap() 創建Map對象,但是還沒有添加數據時,出現的情況。
     * 在無參構造器中,僅僅設置默認加載因子,oldThr還是爲0,oldTab還是爲null。
     * 此時還是屬於沒有初始化哈希表的情況 那麼需要初始化
     * */
    else {
        //容量和閾值都直接初始化爲默認值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    /*4 在上面的三種情況中走完之後,如果新閾值newThr還是等於0,那說明只有情況2會走到這一步
     * 現在計算新的閾值newThr
     * */
    if (newThr == 0) {
        //計算出 新容量*加載因子的值ft,注意在構造器源碼中我們知道加載因子loadFactor可以大於1,因此ft明顯可能大於MAXIMUM_CAPACITY
        float ft = (float) newCap * loadFactor;
        //1 如果新容量小於MAXIMUM_CAPACITY並且新閾值小於MAXIMUM_CAPACITY,那麼新閾值newThr就等於計算出的ft值
        //2 否則,此時有可能是1 新容量等於最大容量,2 新閾值大於等於最大容量,這兩種情況,數組都不能再擴容了。
        // 此時直接設置newThr爲Integer.MAX_VALUE,即變成1.1的情況。
        // 比如使用構造器HashMap( 1, Integer.MAX_VALUE),就會出現這種情況,這種情況可能會造成大量的哈希衝突,
        // 上面的構造器的構造的Map底層數組容量一直是1,後續的數據將一直掛在table[0]的位置處,即一直衝突。
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
                (int) ft : Integer.MAX_VALUE);
    }
    // 將閾值設置爲新閾值
    threshold = newThr;
    /*5 建立新數組
     * 到這裏纔算 真正開始 初始化 or 擴容 ,前面的都是輔助計算*/
    /*新建一個Node數組newTab,容量爲前面計算出的newCap,這個newCap可能是初始化容量也可能是擴容新容量*/
    Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
    //newTab賦值給table,這樣底層table數組變成了新數組,然後還需要數據的轉移
    table = newTab;
    /*6 轉移舊數組的數據
     * 如果原來的table不等於null,嘗試將老table的數據轉移到新的table中,即擴容後轉移數據
     * */
    if (oldTab != null) {
        //從0索引開始,循環整個數組,把每個bucket的元素都移動到新的buckets中;將非空元素進行復制;
        for (int j = 0; j < oldCap; ++j) {
            Node<K, V> e;
            /*獲取數組的第j個元素,使用變量e來保存*/
            /*如果e不爲null,說明這個桶位存在至少一個元素,然後開始處理*/
            if ((e = oldTab[j]) != null) {
                //將舊數組該位置置空
                oldTab[j] = null;
                /*6.1 如果e.next爲null,說明e是這個桶位的唯一一個元素,即該位置沒有哈希衝突,轉移這一個元素*/
                if (e.next == null) {
                    //此時直接再次使用哈希算法計算出該元素在新數組的桶位,然後插入即可
                    newTab[e.hash & (newCap - 1)] = e;
                }
                /*6.2 否則,如果e屬於TreeNode,即紅黑樹節點類型,那麼說明該處桶位是一顆紅黑樹,並且有較嚴重的哈希衝突,開始進行紅黑樹節點的轉移*/
                else if (e instanceof TreeNode) {
                    //調用split方法,將紅黑樹節點也轉移到新數組中,split方法中具有將紅黑樹還原爲鏈表的方法
                    ((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
                }
                /*6.3 否則,那麼此處桶位肯定存在一張鏈表,並且有哈希衝突。且具有2-8個元素,開始進行鏈表節點的轉移*/
                else {
                    /*6.3.1 通過規律找出鏈表中需要移動索引和不需要移動索引的元素 */
                    //存放不需要移動索引位置的鏈表的頭、尾節點
                    Node<K, V> loHead = null, loTail = null;
                    //存放需要移動索引位置的鏈表的頭、尾節點
                    Node<K, V> hiHead = null, hiTail = null;
                    Node<K, V> next;
                    /*do while循環舊鏈表*/
                    do {
                        //獲取下一個節點
                        next = e.next;
                        /*e.hash & oldCap用於比較元素是否需要移動,即比較高一位是否是1還是0,1就需要移動,0則不需要 這是一個規律,具體怎麼得到的,後面的說明中詳細講解*/
                        /*e.hash & oldCap 的結果如果等於0,說明下面是對不需要移動索引位置元素的處理*/
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                //第一次進入外層if時,loTail == null爲true,因此loHead = e
                                loHead = e;
                            else
                                //後續進入外層if時,loTail == null爲false,因此loTail.next=e,相當於記錄一個鏈表
                                loTail.next = e;
                            //每次進入外層if時,將loTail指向e,這樣除了第一次之外,其他時候進入時loTail都不爲null。
                            loTail = e;
                        }
                        /*e.hash & oldCap 的結果如果不等於0,說明下面是對需要移動索引位置元素的處理*/
                        else {
                            //第一次進入外層else時,hiTail == null爲true,因此hiHead = e
                            if (hiTail == null)
                                hiHead = e;
                            else
                                //後續進入外層else時,hiTail == null爲false,因此hiTail.next=e,相當於記錄一個鏈表
                                hiTail.next = e;
                            //每次進入外層else時,將hiTail指向e,這樣除了第一次之外,其他時候進入時hiTail都不爲null。
                            hiTail = e;
                        }
                        //如果e的next不爲null,說明鏈表還沒遍歷到尾部,繼續循環遍歷
                    } while ((e = next) != null);
                    /*6.3.2 將上面找到的兩張舊鏈表遷移到新數組的對應索引位置中 */
                    /*如果loTail不爲null,說明if代碼塊至少進入了一次,或者說明不需要移動索引位置的鏈表有數據*/
                    if (loTail != null) {
                        //到這裏loTail.next實際上可能還會指向其他元素,因此將loTail.next 置爲 null
                        loTail.next = null;
                        //直接將不需要移動索引位置的節點放到新數組的原索引位置處
                        newTab[j] = loHead;
                    }
                    /*如果hiTail不爲null,說明else代碼塊至少進入了一次,或者說明需要移動索引位置的鏈表有數據*/
                    if (hiTail != null) {
                        //到這裏hiTail.next實際上可能還會指向其他元素,因此將hiTail.next 置爲 null
                        hiTail.next = null;
                        //直接將需要移動索引位置的節點放到新數組的(原索引+oldCap)索引位置處
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    /*7 返回新數組*/
    return newTab;
}

5.2.2.1 數據轉移的規律

5.2.2.1.1 特性1

  如果oldCap爲2的冪次方,設e.hash & (oldCap-1) 哈希算法得到的值轉換爲二進制補碼爲a,oldCap直接轉換爲二進制補碼爲b,那麼有這樣的特性:
  b的最高位1及其上面的位數在a中對應的位數全部是0。

  案例:
  設e.hash的二進制補碼爲:0110 0001 0100 0100 0111 0101 1100 1010。設oldCap爲16,那麼oldCap二進制補碼爲:0000 0000 0000 0000 0000 0000 0001 0000。oldCap-1的二進制補碼爲:0000 0000 0000 0000 0000 0000 0000 1111。(實際上2n的正整數轉換爲二進制補碼後,補碼從右向左的第n+1位爲1,其他位數全是0。2n-1轉換爲二進制補碼後,補碼從右向左的第1到第n位全爲1,其他位數全是0。)
  那麼e.hash & (oldCap-1)得到的二進制補碼爲:

0110 0001 0100 0100 0111 0101 1100 1010
0000 0000 0000 0000 0000 0000 0000 1111
——————————————————————
0000 0000 0000 0000 0000 0000 0000 1010

  計算後的結果a=0000 0000 0000 0000 0000 0000 0000 1010,相比與oldCap直接轉換成的二進制補碼0000 0000 0000 0000 0000 0000 0001 0000,b的最高位1及其上面的位數在a中對應的位數全部是0。

5.2.2.1.2 特性2

  如果oldCap爲2的冪次方,當容量擴容爲兩倍之後,newCap的二進制補碼相對於oldCap的二進制補碼來說,最高位的1左移了一位。
  案例:
  newCap=32時,它的二進制補碼爲:0000 0000 0000 0000 0000 0000 0010 0000,相比16的二進制補碼,最高位的1確實左移了一位。

5.2.2.1.3 數據轉移位置的規律

  有了上面的兩個特性,我們可以說,老節點在新數組中的位置計算和在舊數組的位置計算,在二進制下的差別就在於新數組的位置計算中新增了一個的高位參與計算。所以老節點在新數組中的位置和原位置的區別實際上就取決於這個高位的運算結果。最終的規律是:

  1. 如果老節點的hash的對應新增高位是1,那麼&之後的高位結果還是1,那麼老節點在新數組的位置實際上就是(原索引+oldCap),即原索引加上舊數組容量。
  2. 如果老節點的hash的對應新增高位是0,那麼&之後的高位結果變成0,那麼老節點在新數組的位置實際上還是原索引位置,即沒改變。
  3. 還能總結出:舊數組的索引k的節點,在新數組中的位置只能是索引k或者k+oldCap兩者中的一個!

  案例:
  有兩個key,k1.hash=0110 0001 0100 0100 0111 0101 1100 1010,k2.hash=0110 0001 0100 0100 0111 0101 1101 1010。oldCap=16,此時計算出來的位置如下:
k1

k1.hash    0110 0001 0100 0100 0111 0101 1100 1010
oldCap-1  0000 0000 0000 0000 0000 0000 0000 1111
——————————————————————————
index1     0000 0000 0000 0000 0000 0000 0000 1010

k2

k2.hash    0110 0001 0100 0100 0111 0101 1101 1010
oldCap-1  0000 0000 0000 0000 0000 0000 0000 1111
——————————————————————————
index2     0000 0000 0000 0000 0000 0000 0000 1010

  從結果可以看出來,不同的hash計算出來的結果是一樣的,即都位於10的位置。假設key不相等,那麼這個位置就存放了這兩個元素節點。
  在擴容之後,newCap=32。,此時採用傳統方法重新計算位置。計算出來的位置如下:
k1

k1.hash    0110 0001 0100 0100 0111 0101 1100 1010
newCap-1  0000 0000 0000 0000 0000 0000 0001 1111
——————————————————————————
index3     0000 0000 0000 0000 0000 0000 0000 1010

k2

k2.hash    0110 0001 0100 0100 0111 0101 1101 1010
newCap-1  0000 0000 0000 0000 0000 0000 0001 1111
——————————————————————————
index4     0000 0000 0000 0000 0000 0000 0001 1010

  上面的案例也能看出來,我們所說的新增參與計算的一個高位是怎麼回事兒了。並且index3=index1,而index4= index2+16。因此我們可以有這樣的規律!

5.2.2.1.4 e.hash & oldCap

  有了上面的規律。我們直接判斷老節點的hash的對應高位是否爲0或者1,就能知道這個節點在新數組中的位置,使用這個規律,相比於每次都重新進行哈希算法的計算,提升了效率!
  那麼,怎麼來判斷e.hash的新增高位是否是0還是1呢?我們還發現,實際oldCap直接轉換爲二進制補碼之後,它的唯一的那個1正好對應着e.hash新增的高位,並且其他位數全是0。
  使用e.hash & oldCap,由於oldCap除了高位1之外,其他爲全是0,那麼最終的值就去取決於e.hash的對應新增高位的值了:

  1. 如果最終值等於0,那麼說明e.hash的對應新增高位爲0,該數據就存儲在新數組的原索引處;
  2. 否則說明e.hash的對應高位爲1,該數據就存儲在新數組的原索引+oldCap的索引位置處

  案例:
k1

k1.hash  0110 0001 0100 0100 0111 0101 1100 1010
oldCap   0000 0000 0000 0000 0000 0000 0001 0000
——————————————————————
result1   0000 0000 0000 0000 0000 0000 0000 0000

k2

k2.hash  0110 0001 0100 0100 0111 0101 1101 1010
oldCap   0000 0000 0000 0000 0000 0000 0001 0000
——————————————————————
result2   0000 0000 0000 0000 0000 0000 0001 0000

  我們看到,k1、k2和oldCap進行&運算之後的result1等於0,而result2不等於0,因此可以通過判斷e.hash & oldCap的值是否等於0來判斷,新增參與計算的高位是0還是1。
  JDK1.8使用上面的規律來計算同一個桶位置的節點新位置的方法,相對於JDK1.7的對每個元素重新使用哈希算法【hashCode()–> 擾動處理 -->hash & (length-1)】來獲取新位置的方法,代碼簡單了不少,同時提升了效率。並且JDK1.8中舊鏈表節點遷移到新數組之後,在原同一張鏈表中的元素相對位置無變化;在JDK在1.7中,舊鏈表節點遷移到新數組之後,在原同一張鏈表中的元素相對位置變成了倒置;

5.2.2.2 鏈表數據轉移源碼

  上面講了數據轉移的規律,現在來看看JDK1.8的HashMap是如何利用這個規律來快速轉移鏈表數據的,轉移鏈表數據大概分爲兩步:

  1. 將老索引位置k的全部節點,拆分成不需要移動索引位置和需要移動索引位置的兩條鏈表;
  2. 將這兩條鏈表頭節點分別賦值給新數組的k和k+oldCap索引位置處,轉移結束!

  兩張鏈表的構建在源碼中是這麼寫:

//存放不需要移動索引位置的節點
Node<K, V> loHead = null, loTail = null;
//存放需要移動索引位置的節點
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
/*do while循環舊鏈表*/
do {
    //獲取下一個節點
    next = e.next;
    /*e.hash & oldCap用於比較元素是否需要移動,即比較高一位是否是1還是0,1就需要移動,0則不需要 這是一個規律,具體怎麼得到的,後面的說明中詳細講解*/
    /*e.hash & oldCap 的結果如果等於0,說明下面是對不需要移動索引位置元素的處理*/
    if ((e.hash & oldCap) == 0) {
        if (loTail == null)
            //第一次進入外層if時,loTail == null爲true,因此loHead = e
            loHead = e;
        else
            //後續進入外層if時,loTail == null爲false,因此loTail.next=e,相當於記錄一個鏈表
            loTail.next = e;
        //每次進入外層if時,將loTail指向e,這樣除了第一次之外,其他時候進入時loTail都不爲null。
        loTail = e;
    }
    /*e.hash & oldCap 的結果如果不等於0,說明下面是對需要移動索引位置元素的處理*/
    else {
        //第一次進入外層else時,hiTail == null爲true,因此hiHead = e
        if (hiTail == null)
            hiHead = e;
        else
            //後續進入外層else時,hiTail == null爲false,因此hiTail.next=e,相當於記錄一個鏈表
            hiTail.next = e;
        //每次進入外層else時,將hiTail指向e,這樣除了第一次之外,其他時候進入時hiTail都不爲null。
        hiTail = e;
    }
    //如果e的next不爲null,說明鏈表還沒遍歷到尾部,繼續循環遍歷
} while ((e = next) != null);

  上面的代碼光說文字肯定有點繞,但是畫張圖一下就明白了,這兩張鏈表的構建原理是一樣的。首先是一張舊的哈希表:
在這裏插入圖片描述
  我們以第一個桶位置的鏈表爲案例來講解!
  最開始,e=e1,loHead、loTail、hiHead、hiTail、next都爲null。
  第1次執行do代碼塊,e=e1,next= e .next=e2,假設e.hash & oldCap等於0,那麼第1次進入第一個if塊,由於loTail爲null,因此loHead=e,loTail=e,代碼結束,此時變量的引用關係爲:
在這裏插入圖片描述
  執行while條件,e = next=e2,判斷e2不爲null,那麼開始第2次執行do代碼塊,e=e2,next= e .next=e3,假設e.hash & oldCap不等於0,那麼第1次進入第二個if塊,由於hiTail爲null,因此hiHead=e,hiTail=e,代碼結束,此時變量的引用關係爲:
在這裏插入圖片描述
  執行while條件,e = next=e3,判斷e3不爲null,那麼開始第3次執行do代碼塊,e=e3,next= e .next=e4,假設e.hash & oldCap不等於0,那麼第2次進入第二個if塊,由於hiTail不爲null,因此hiTail.next = e,hiTail=e,代碼結束,此時變量的引用關係爲:
在這裏插入圖片描述
  執行while條件,e = next=e4,判斷e4不爲null,那麼開始第4次執行do代碼塊,e=e4,next= e .next=e5,假設e.hash & oldCap等於0,那麼第2次進入第一個if塊,由於loTail不爲null,因此loTail.next = e,loTail=e,代碼結束,此時變量的引用關係爲:
在這裏插入圖片描述
  執行while條件,e = next=e5,判斷e5不爲null,那麼開始第5次執行do代碼塊,e=e5,next= e .next=e6,假設e.hash & oldCap不等於0,那麼第3次進入第二個if塊,由於hiTail不爲null,因此hiTail.next = e,hiTail=e,代碼結束,此時變量的引用關係爲:
在這裏插入圖片描述
  執行while條件,e = next=e6,判斷e6不爲null,那麼開始第6次執行do代碼塊,e=e6,next= e .next=null,假設e.hash & oldCap等於0,那麼第3次進入第一個if塊,由於loTail不爲null,因此loTail.next = e,loTail=e,代碼結束,此時變量的引用關係爲:
在這裏插入圖片描述
  執行while條件,e = next=e6,判斷e6爲null,那麼do while循環結束,最終的引用指向結果爲:
在這裏插入圖片描述
  最終我們可以看到, loHead指向了不需要移動索引位置的鏈表節點頭部,loTail指向不需要移動索引位置的鏈表節點尾部;hiHead指向了需要移動索引位置的鏈表節點頭部,hiTail指向需要移動索引位置的鏈表節點尾部。
  但是我們還發現,可能會出現某一張鏈表的尾節點的next節點不爲null,因此還需要進一步處理,對於這個情況的處理,是在第二步插入到新數組時完成的!
  插入鏈表到新數組的源碼如下:

/*如果loTail不爲null,說明if代碼塊至少進入了一次,或者說明不需要移動索引位置的鏈表有
數據*/
if (loTail != null) {
    //到這裏loTail.next實際上可能還會指向其他元素,因此將loTail.next 置爲 null
    loTail.next = null;
    //直接將不需要移動索引位置的節點放到新數組的原索引位置處
    newTab[j] = loHead;
}
/*如果hiTail不爲null,說明else代碼塊至少進入了一次,或者說明需要移動索引位置的鏈表有數據*/
if (hiTail != null) {
    //到這裏hiTail.next實際上可能還會指向其他元素,因此將hiTail.next 置爲 null
    hiTail.next = null;
    //直接將需要移動索引位置的節點放到新數組的(原索引+oldCap)索引位置處
    newTab[j + oldCap] = hiHead;
}

  JDK1.8中 舊鏈表遷移到新數組,在原同一張鏈表中的元素相對位置沒有變化;在JDK在1.7中,舊鏈表遷移到新數組,在原同一張鏈表中的元素相對位置變成了倒置(頭插法);
  通過數據轉移的規律和上面兩個步驟,我們在向新數組轉移原數組某個桶位置的節點時,直接將這計算出來的鏈表插入通過規律計算出的新數組對應索引處,方便快捷!

5.2.2.3 紅黑樹數據轉移split方法

  紅黑樹的數據節點轉移是一個單獨的方法:split。它的內部源碼起始也比較複雜,但是仍然可以分爲兩步:

  1. 將老索引位置k的全部節點,拆分成不需要移動索引位置和需要移動索引位置的兩條鏈表,並記錄兩條鏈表的長度;
  2. 將這兩條鏈表分別轉移到新數組的k和k+oldCap索引位置處,在這個過程中需要判斷兩條鏈表的長度小於等於6就調用“樹鏈表還原爲普通鏈表”的方法untreeify存儲到新位置,否則選擇“樹形化”的方法treeify形成新的紅黑樹存儲到新位置。
/**
 * 紅黑樹節點類型的數據轉移
 * @param map   當前hashMap對象
 * @param tab   新數組
 * @param index 需要轉移的紅黑樹的位置,舊數組索引
 * @param bit   舊數組容量
 */
final void split(HashMap<K, V> map, Node<K, V>[] tab, int index, int bit) {
    TreeNode<K, V> b = this;
    /*1 類似於鏈表的轉移的第一步,將老索引位置的全部節點,拆分成不需要移動索引位置和需要移動索引位置的兩條鏈表;
    * 爲什麼紅黑樹也能採用這種方法?因爲TreeNode間接的繼承了Node,自然具有Node的所有屬性和方法
    * 並且在轉換爲紅黑樹或者插入紅黑樹節點時,實際上TreeNode之間還維護了插入的先後關係的next字段
    * */
    //存放不需要移動索引位置的鏈表的頭、尾節點
    TreeNode<K, V> loHead = null, loTail = null;
    //存放需要移動索引位置的鏈表的頭、尾節點
    TreeNode<K, V> hiHead = null, hiTail = null;
    //存放不需要\需要移動索引位置的鏈表的長度
    int lc = 0, hc = 0;
    //由於TreeNode之間通過next維護了先後順序,因此同樣循環遍歷就可以了
    for (TreeNode<K, V> e = b, next; e != null; e = next) {
        next = (TreeNode<K, V>) e.next;
        e.next = null;
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        } else {
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
    /*2 將上面找到的兩張舊鏈表遷移到新數組的對應索引位置中,相比於鏈表的遷移更加複雜*/
    //判斷不需要移動索引位置的鏈表有數據
    if (loHead != null) {
        //如果不需要移動索引位置的鏈表長度小於等於UNTREEIFY_THRESHOLD,即小於等於6
        if (lc <= UNTREEIFY_THRESHOLD)
            //那麼將loHead鏈表的紅黑樹節點轉換爲鏈表節點存儲
            //樹還原爲鏈表,untreeify將返回普通鏈表頭節點
            tab[index] = loHead.untreeify(map);
        else {
            //否則,那麼將loHead鏈表轉換爲紅黑樹存儲
            tab[index] = loHead;
            if (hiHead != null) // (else is already treeified)
                //樹形化。該方法在“樹形化treeifyBin方法”部分有源碼講解。
                loHead.treeify(tab);
        }
    }
    //判斷需要移動索引位置的鏈表有數據
    if (hiHead != null) {
        //如果需要移動索引位置的鏈表長度小於等於UNTREEIFY_THRESHOLD,即小於等於6
        //那麼將loHead鏈表的紅黑樹節點轉換爲鏈表節點存儲
        if (hc <= UNTREEIFY_THRESHOLD)
            //樹還原爲鏈表,untreeify將返回普通鏈表頭節點
            tab[index + bit] = hiHead.untreeify(map);
        else {
            //否則,那麼將loHead鏈表轉換爲紅黑樹存儲
            tab[index + bit] = hiHead;
            if (loHead != null)
                //樹形化。該方法在“樹形化treeifyBin方法”部分有源碼講解。
                hiHead.treeify(tab);
        }
    }
}

  樹形化方法treeify在該方法在“樹形化treeifyBin方法”部分有講解;樹還原的方法untreeify下面講解!

5.2.2.3.1 樹還原untreeify方法

  樹還原方法untreeify用於將紅黑樹鏈表轉換爲普通節點鏈表,在刪除節點以及樹節點轉移過程中都可能會調用到。
  原理很簡單:遍歷紅黑樹節點鏈表,將每個紅黑樹節點轉換爲普通節點,然後保存他們的next引用關係即可。

/**
 * 樹還原爲普通鏈表的方法,由紅黑樹鏈表頭節點調用
 *
 * @param map 當前Map集合
 * @return 普通節點鏈表頭節點
 */
final Node<K, V> untreeify(HashMap<K, V> map) {

    Node<K, V> hd = null, tl = null;
    //循環該紅黑樹鏈表,轉換爲普通節點鏈表,節點順序還是原來的順序
    for (Node<K, V> q = this; q != null; q = q.next) {
        //將每一個樹節點轉換爲普通節點,主次此事next關係沒有記錄,在下面會記錄
        Node<K, V> p = map.replacementNode(q, null);
        /*類似於轉移節點時的鏈表構造原理*/
        //首次for循環時,tl爲null,將會進入該代碼塊
        if (tl == null)
            //紅黑樹鏈表頭節點作爲普通鏈表頭節點
            hd = p;
        //後續循環時,tl不爲null,將會進入該代碼塊
        else
            //這裏重新關聯next關係
            tl.next = p;
        //tl指向該新節點
        tl = p;
    }
    //普通節點鏈表頭節點
    return hd;
}


/**
 * 用於從樹節點轉換爲普通節點
 *
 * @param p    樹節點
 * @param next next引用
 * @return 普通節點
 */
Node<K, V> replacementNode(Node<K, V> p, Node<K, V> next) {
    //返回新建的普通節點,存入樹節點的內容
    return new Node<>(p.hash, p.key, p.value, next);
}

5.2.3 尾插法插入節點

5.2.3.1 尾插法和頭插法

  JDK1.8的HashMap在插入節點時,採用的“尾插法”,尾插法很好理解,實際上就是將新的節點鏈接到原鏈表的尾部,使之成爲新的尾節點。而JDK1.8之前的HashMap或者全部版本的Hashtable則是採用的“頭插法”,顧名思義,頭插法就是將新節點鏈接到鏈表頭部,使之成爲新的頭節點。
  在擴容轉移元素時,JDK1.7的HashMap也是採用的頭插法,這樣造成了元素的相對位置變成了倒置;而JDK1.8由於採用了計算規律,並且獲得的兩個鏈表的元素相對順序並沒有變,也相當於“尾插法”的結果。
  採用頭插法的思想很簡單,那就是後來插入的元素可能會被更大概率的訪問到,那麼插入的鏈表頭部相比於插入鏈表尾部,能夠節省更多的遍歷時間,提升效率。但是後來人們發現,對於HashMap這種非線程安全的集合,在多線程下在擴容時採用頭插法可能會構造一個循環鏈表,在遍歷時造成死循環,因此JDK1.8的HashMap改爲“尾插法”!

5.2.3.2. 併發擴容導致循環鏈表原理

  在JDK1.7中,由於擴容時使用頭插法,在併發時可能會形成循環列表,導致死循環,在JDK1.8中改爲尾插法,可以避免這種問題,但是依然避免不了節點丟失的問題。
  先來看看JDK1.7轉移數據的源碼,大概就是:循環遍歷舊數組,然後循環遍歷每一個鏈表,將每個鏈表節點通過頭插法插入到新數組。
  下面的源碼中有兩個關鍵位置:“關鍵位置1”和“關鍵位置2”,後續會用到!

void transfer(Entry[] newTable, boolean rehash) {
    //新數組容量
    int newCapacity = newTable.length;
    //舊數組,將桶爲的每一個鏈表轉移到新數組中,注意這裏是從鏈表的頭部開始遍歷的
    for (Entry<K,V> e : table) {
        while(null != e) {
            //關鍵位置1
            Entry<K,V> next = e.next;
            //關鍵位置2
            /*hashCode-->擾動算法-->h & (length-1) 計算在新數組中的位置*/
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            /*這三步,就是頭插法的運用,後續的節點將會插入到新數組鏈表頭部*/
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

  設採用HashMap(2,1)的方式創建了哈希表,並且加入了兩個元素e1,e2並且發生了衝突,此時該哈希表的結構如下。注意JDK1.7的HashMap是先通過(size >= threshold) && (null != table[bucketIndex])判斷是否需要擴容並且在擴容之後,再添加元素的。
在這裏插入圖片描述
  當添加第三個元素e3時,明顯size=2等於threshold=2,假設第三個元素同樣計算的位置和另兩個元素位於同一個桶內,那麼需要擴容!
  如果此時有兩個線程A、B調用put方法,那麼它們檢測到的結果是都需要擴容,假設他們都執行到了transfer方法。線程A運行到關鍵位置2時,此時線程A中e=e1,next=e2,然後切換到了線程B,並且將transfer方法執行完畢,假設e1和e2計算出來的在新數組中的位置一樣,那麼此時的新table數組可能是:
在這裏插入圖片描述
  然後又切換到了線程A,注意在transfer方法之前,兩個線程自個建立了屬於自己線程的新數組,但是內部元素卻是兩個線程共享的。
  此時線程A中的A中e=e1,next=e2。線程A繼續執行,e.next=newTable[i],即e.next=null,然後將newTable[i] = e,即將e1頭插法遷移到自己的新數組中,然後e=next,即將e2賦值給e,此時A線程中新數組狀態爲:
在這裏插入圖片描述
  第二次執行while,此時e=e2,由於線程B改變了e1和e2的引用關係,導致next=e。next=e1。同頭插法插入e2之後,e=next=e1,此時A線程中新數組狀態爲:
在這裏插入圖片描述
  第三次執行while,此時e=e1,因此還需要循環。此時next=e.next=null。e.next=newTable[i],即e.next=e2,然後newTable[i] = e,即將e1頭插法遷移到自己的新數組中,然後e=next=null,此時A線程中新數組狀態爲:
在這裏插入圖片描述
  第四次,然後e =null,while循環結束,這樣形成了循環鏈表,我們知道在查找元素的時候,需要遍歷整個鏈表,那麼如果是循環鏈表,就沒有了尾節點,遍歷永遠不會結束,造成死循環!

5.2.3.3 併發擴容導致節點丟失原理

  同樣,由於兩個線程都具有自己的新數組,那麼在給共享變量table賦值時,會造成兩個數組的相互覆蓋,這樣也能造成數據節點的丟失。在JDK1.8中改爲尾插法,可以避免死循環的問題,但是依然避免不了節點丟失的問題。
  我們假設e1和e2在新數組的位置不一樣,如果線程A執行到關鍵位置1,就切換到了線程B,此時線程A中e=e1。
  線程B將transfer方法執行完畢。然後切換到A線程,繼續執行,next = e.next,由於e1和e2的引用關係已被線程B去除了,此時next = e1.next=null,因此transfer方法執行完畢之後,線程B、A中的新數組結果如下:
在這裏插入圖片描述
  transfer方法執行完畢之後,緊跟着就是將新數組賦值給共享變量table,如果B先複製,A後賦值,那麼A的數組將覆蓋B的數組,此時將造成數據節點的丟失!
  數據節點丟失的情況,無論是JDK1.7還是JDK1.8的HashMap都不能避免,因此有併發的場景,推薦使用ConcurrentHashMap。

5.2.4 樹形化treeifyBin方法

  當添加新節點之後的鏈表長度大於8,那麼將該鏈表轉換爲紅黑樹,使用的就是treeifyBin方法。JDK1.8的HashMap的紅黑樹最初的由來就是通過該方法構造的!
  注意:在該方法裏面還會判斷當哈希表中的容量大於等於MIN_TREEIFY_CAPACITY,即64 時,才允許樹形化鏈表,否則不進行樹形化,而是擴容。
  treeifyBin方法可以分爲以下幾步:

  1. 如果舊數組爲空,或者容量小於MIN_TREEIFY_CAPACITY,即小於64,那麼進行數組擴容,方法結束。
  2. 否則,可以開始樹形化:
    a) 循環普通鏈表,將普通節點鏈表,轉換爲紅黑樹節點鏈表,順序還是原來的順序;
    b) 紅黑樹鏈表頭節點調用treeify方法,由紅黑樹節點鏈表構建成爲紅黑樹,方法結束。
/**
 * 當添加新節點之後的鏈表長度大於8,那麼將該鏈表轉換爲紅黑樹,使用的就是treeifyBin方法。
 * JDK1.8的HashMap的紅黑樹最初的由來就是通過該方法構造的!
 *
 * @param tab  舊數組
 * @param hash key的hash值
 */
final void treeifyBin(Node<K, V>[] tab, int hash) {
    int n, index;
    Node<K, V> e;
    /*1 如果舊數組爲空,或者容量小於MIN_TREEIFY_CAPACITY,即小於64,那麼進行數組擴容*/
    //從這裏可以看出來,想要進行樹形化,那麼需要 某個桶位的鏈表長度大於8,同時需要數組容量大於等於64
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
        //擴容一次,方法返回
        resize();
    }
    /*2 否則,可以開始樹形化*/
    //計算鏈表索引位置
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //hd保存紅黑樹鏈表的頭部,tl保存紅黑樹鏈表的尾部
        TreeNode<K, V> hd = null, tl = null;
        /*2.1 循環普通鏈表,將普通節點鏈表,轉換爲紅黑樹節點鏈表,順序還是原來的順序*/
        do {
            //新建紅黑樹節點,注意這裏沒有保存了節點之間的next的關係
            TreeNode<K, V> p = replacementTreeNode(e, null);
            //第一次進入do代碼塊時,tl爲null
            if (tl == null)
                //hd賦值爲p
                hd = p;
                //後續進入do代碼塊時,tl不爲null
            else {
                //p的前驅設置爲tl,這裏保存了節點之間的前驅關係
                p.prev = tl;
                //tl的後繼設置爲p,這裏保存了節點之間的後繼關係
                tl.next = p;
            }
            //每次,tl賦值爲p
            tl = p;
        } while ((e = e.next) != null);
        //數組的槽位暫時存入紅黑樹鏈表的頭節點,後面還會調整爲紅黑樹的根節點(moveRootToFront方法中)
        if ((tab[index] = hd) != null)
            /*2.2 紅黑樹鏈表頭節點調用treeify方法,由紅黑樹節點鏈表構建成爲紅黑樹*/
            hd.treeify(tab);
    }
}


/**
 * 新建紅黑樹節點,注意這裏沒有保存了節點之間的next的關係
 *
 * @param p    鏈表節點,從頭節點開始
 * @param next 下一個節點引用,爲null
 * @return 紅黑樹節點,顏色屬性red默認是false,即黑色。
 */
TreeNode<K, V> replacementTreeNode(Node<K, V> p, Node<K, V> next) {
    return new TreeNode<>(p.hash, p.key, p.value, next);
}

  下面詳細講解,如何通過紅黑樹鏈表構建紅黑樹的!

5.2.4.1 構建紅黑樹treeify方法

  treeify方法用於將紅黑樹鏈表構建成爲紅黑樹,在新增節點樹形化以及樹節點轉移過程中都可能會調用到。大概步驟有如下幾步:

  1. 遍歷紅黑樹鏈表,獲取每一個紅黑樹節點。
    a) 第一次直接將第一個節點當成紅黑樹的根節點,後續對每個節點採用排序二叉樹的方法,在經過一系列比較大小的方法之後插入到紅黑樹中;
    b) 每次插入完成之後對紅黑樹調用balanceInsertion方法進行重平衡操作,並獲取新的根節點。
  2. 遍歷鏈表結束之後(也標誌着紅黑樹轉換完畢),調用moveRootToFront方法調整根節點的next和prev引用並設置爲對應數組索引位置的節點,即tab[i]=root。
/**
 * 由紅黑樹節點鏈表構建成爲紅黑樹
 *
 * @param tab 舊數組
 */
final void treeify(Node<K, V>[] tab) {
    //樹根節點
    TreeNode<K, V> root = null;
    /*遍歷鏈表,x指向當前調用節點*/
    for (TreeNode<K, V> x = this, next; x != null; x = next) {
        //next指向下一個節點,一次循環之後x指向next
        next = (TreeNode<K, V>) x.next;
        //設置當前節點的左右節點爲空,在擴容方法那裏調用該方法時會用到,
//因爲擴容時原紅黑樹鏈表節點保存了以前的舊的關係,現在需要重新確定關係,因此先清空舊的關係
        x.left = x.right = null;
        /*1 如果沒有根節,那麼x成爲根節點,在treeifyBin方法中,是由紅黑樹鏈表的頭節點hd調用的該方法*/
        if (root == null) {
            //根節點的父節點指向null
            x.parent = null;
            //顏色設置爲false,即黑色
            x.red = false;
            //root指向根節點
            root = x;
        }
        /*2 如果有根節點*/
        else {
            K k = x.key;
            int h = x.hash;
            //key所屬類型
            Class<?> kc = null;
            /*從根節點開始遍歷紅黑樹*/
            for (TreeNode<K, V> p = root; ; ) {
                //dir 標識左右,-1表示左側,1表示右側
                //ph保存當前樹節點的hash值
                int dir, ph;
                //當前樹節點key
                K pk = p.key;
                //如果當前樹節點p的hash值大於x節點的hash值
                if ((ph = p.hash) > h)
                    //-1表示x節點會放到當前樹節點的左側
                    dir = -1;
                    //否則,如果當前樹節點p的hash值小於x節點的hash值
                else if (ph < h)
                    //1表示x節點會放到當前樹節點的右側
                    dir = 1;
                    //否則,如果當前樹節點p的hash值等於x節點的hash值,那麼比較k和pk的大小
                    //如果x的key的類型kc等於null,那麼kc等於comparableClassFor(k),如果k的類型屬於Comparable類型,那麼kc不爲null,否則kc=null
                    //由於||運算是短路法運算,因此如果kc==null爲真,並且kc = comparableClassFor(k)) == null 也爲真,那麼||後面的表達式將不會執行,
                    //此時表示k不屬於Comparable類型,然後使用tieBreakOrder比較得出最終結果。
                else if ((kc == null &&
                        (kc = comparableClassFor(k)) == null) ||
                        //如果kc==null爲true,並且kc = comparableClassFor(k)) == null 爲false,或者kc==null爲false,那麼表示k屬於Comparable類型,此時前面的表達式爲false,
                        // 那麼||後面的表達式將會執行,即使用compareComparables方法比較 ,compareComparables實際上就是將兩個k使用compareTo方法比較
                        // 如果得到的結果爲0,那麼表示相等,後面的表達式返回true,此時再通過tieBreakOrder比較一次計算出最終結果
                        (dir = compareComparables(kc, k, pk)) == 0)
                    //如果k的類型不是Comparable類型,或者k和pk使用compareComparables比較返回結果爲0,那麼最終調用tieBreakOrder方法進行比較
                    //最終比較k和kp大小的方法,實際上是比較k和pk的類名字符串或者比較k和pk的identityHashCode的大小,最後只會返回-1或者1
                    dir = tieBreakOrder(k, pk);
                //使用xp保存當前樹節點p
                TreeNode<K, V> xp = p;
                //如果dir小於等於0,那麼x節點一定在當前樹節點p的左側;否則,如果dir 大於0,那麼x節點一定在當前樹節點p的右側
                //由於僅僅知道x是在p的左側或者右側,不知道p是否有左、右子樹,如果已經存在了左右子樹,那麼還需要遞歸子樹,直到查找到對應位置爲null,因此還需要判斷:
                //如果x是在p的左側,並且當p的左子樹爲null時,直接使x成爲xp的左子節點,x.parent指向xp,然後xp.left指向x
                //如果x是在p的右側,並且當p的右子樹爲null時,直接使x成爲xp的右子節點,x.parent指向xp,然後xp.right指向x
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    //上面的插入實際上就是排序二叉樹的插入方式,對與紅黑樹來說,在調用二叉排序樹的插入方式成功插入節點之後,可能會破壞紅黑樹的平衡,因此還需要重平衡操作,
                    //因此在最後調用balanceInsertion(root, x)方法對紅黑樹進行重平衡,然後返回平衡之後的新的根節點
                    root = balanceInsertion(root, x);
                    //導致成功的插入了紅黑樹節點,並且調整了平衡,結束該次紅黑樹的遍歷,繼續下一個鏈表節點的插入
                    break;
                }
            }
        }
    }
    //將鏈表完全轉換爲紅黑樹之後,紅黑樹可能經歷了多次再平衡,需要將最後的root節點,設置爲數組節點,即tab[i]=root
    //同時需要調整root放入next和prev的引用指向,將root節點作爲鏈表的頭節點
    moveRootToFront(tab, root);
}

  下面針對重要方法進行講解!

5.2.4.1.1 比較節點大小的方法

  由於紅黑樹屬於二叉排序樹的一種,因此紅黑樹的節點之間必須具有大小關係,這樣才能構建紅黑樹。即使HashMap的元素是無序的,即使元素沒有實現Comparable接口,在HashMap內部的紅黑樹中也會採用自己的方法對節點進行比較排序!

5.2.4.1.1.1 comparableClassFor方法

  對象x的類型爲c,如果對象x實現了Comparable< c >接口,那麼返回對象x的類型c,否則返回null。

/**
 * 對象x的類型爲c,如果對象x實現了Comparable< c >接口,那麼返回對象x的類型c,否則返回null
 */
static Class<?> comparableClassFor(Object x) {
    /*x是否屬於Comparable類型*/
    if (x instanceof Comparable) {
        //如果x屬於Comparable類型,還需要進一步判斷Comparable接口的泛型類型!
        Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
        //如果x的類型c是String類型,即key爲String類型,那麼直接返回c。
        //因爲String類型實現了Comparable<String>接口
        if ((c = x.getClass()) == String.class) // bypass checks
            return c;
        //否則,獲取c直接實現的接口類型數組,如果是泛型接口(參數化類型)還會返回泛型的信息,簡單的說具有<>符號的接口是參數化類型
        if ((ts = c.getGenericInterfaces()) != null) {
            //遍歷該數組
            for (int i = 0; i < ts.length; ++i) {
                //(t = ts[i]) instanceof ParameterizedType  -->如果t是個泛型接口(參數化類型),
                //(p = (ParameterizedType)t).getRawType() == Comparable.class   -->如果該接口的類型是Comparable類型
                //as = p.getActualTypeArguments()) != null   -->如果該接口具有泛型參數
                //as.length == 1    -->如果該接口的泛型參數個數爲1個
                //as[0] == c  -->如果該接口的泛型參數類型爲c
                //如果以上條件全部滿足,說明x實現了Comparable接口,並且Comparable接口的泛型參數類型就是x的類型c,那麼同樣直接返回c
                if (((t = ts[i]) instanceof ParameterizedType) &&
                        ((p = (ParameterizedType)t).getRawType() ==
                                Comparable.class) &&
                        (as = p.getActualTypeArguments()) != null &&
                        as.length == 1 && as[0] == c) // type arg is c
                    return c;
            }
        }
    }
    //如果x不屬於Comparable類型,或者實現Comparable接口的泛型類型不是x的所屬類型,那麼最終返回null
    return null;
}
5.2.4.1.1.2 compareComparables方法

  如果kc == null爲true,並且kc = comparableClassFor(k)) == null 爲false,或者kc==null爲false,那麼表示kc一定是屬於Comparable類型。
  此時前面的表達式爲false,那麼||後面的表達式將會執行,即使用compareComparables方法進一步比較 ,compareComparables實際上就是將兩個k使用compareTo方法比較。
  如果x(pk)爲null,或者x(pk)的類型不是kc,則返回0;否則返回k.compareTo(x)的比較結果。

/**
 * 如果x爲null,或者x的類型不是kc,則返回0;否則返回k.compareTo(x)的比較結果
 *
 * @param kc x.key的類型,執行到這個方法,kc一定是Comparable類型
 * @param k  x.key
 * @param x  當前樹節點p的key,pk
 * @return 如果x爲空,或者x的類型不是kc,則返回0;否則返回k.compareTo(x)的比較結果
 */
static int compareComparables(Class<?> kc, Object k, Object x) {
    return (x == null || x.getClass() != kc ? 0 :
            ((Comparable) k).compareTo(x));
}
5.2.4.1.1.3 tieBreakOrder方法

  如果k的類型不是Comparable類型,或者k和pk使用compareComparables比較返回結果爲0,即不具備compareTo比較資格或者compareTo比較之後仍然沒有分出大小,那麼最終調用tieBreakOrder方法進行比較。
  實際上tieBreakOrder方法是比較k和pk的類名字符串或者比較k和pk的identityHashCode的大小,最後只會返回-1或者1。

/**
 * 如果k的類型不是Comparable類型,或者k和pk使用compareComparables比較返回結果爲0。
 * 即不具備compareTo比較資格或者compareTo比較之後仍然沒有分出大小,那麼最終調用tieBreakOrder方法進行比較。
 *
 * @param a x節點的key,k
 * @param b 當前樹節點的key,pk
 * @return 只會返回 1 或者 -1
 */
static int tieBreakOrder(Object a, Object b) {
    int d;
    //a == null --> 如果a等於null
    //b == null --> 如果a不等於null,b等於null
    //(d = a.getClass().getName().compareTo(b.getClass().getName())) == 0
    // -->如果a不等於null,b不等於null,那麼比較兩個對象的類名字符串,按照字符串的比較方法返回結果d
    //如果a不等於null,b不等於null
    if (a == null || b == null ||
            (d = a.getClass().getName().
                    compareTo(b.getClass().getName())) == 0)
        //如果a不等於null,b不等於null,並且a、b類名字符串的比較結果還是返回0
        //那麼調用identityHashCode方法獲取a、b的本地哈希碼進行比較,如果a小於等於b則返回-1,否則返回1
        //identityHashCode():即無論對象有沒有重寫hashcode()方法,都調用Object類的原始hashCode()方法。
        d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
                -1 : 1);
    return d;
}
5.2.4.1.2 插入後調整平衡balanceInsertion方法

  該方法在紅黑樹節點成功插入之後調用,重新平衡紅黑樹,用於維持紅黑樹的性質!
  關於插入之後的平衡,有很多種情況,我們只需要根據情況按部就班的採用對應的方法就行了,在紅黑樹的原理和實現部分已經有了插入平衡的詳細講解,這裏不再多說:數據結構—紅黑樹(RedBlackTree)的實現原理以及Java代碼的完全實現

/**
 * 該方法在紅黑樹節點成功插入之後調用,重新平衡紅黑樹,用於維持紅黑樹的性質!
 * 關於插入之後的平衡,有很多種情況,我們只需要根據情況按部就班的採用對應的方法就行了,
 * 在紅黑樹的原理和實現部分已經有了插入平衡的詳細講解,實際上不是很複雜的,
 * 這裏不再多說,只是將每種情況和紅黑樹原理那兒的每種情況的簡稱對應上來,這樣就很容易理解了!
 * 同樣假設,null節點爲黑色節點
 *
 * @param root 平衡之前的根節點
 * @param x    新插入的節點
 * @return 平衡之後的根節點
 */
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root, TreeNode<K, V> x) {
    //新插入節點設置爲紅色
    x.red = true;
    //xp表示x節點的父節點,xpp表示x節點的祖父節點,xppl表示左叔節點,xppr表示右叔節點
    for (TreeNode<K, V> xp, xpp, xppl, xppr; ; ) {
        /*1 如果新節點的父節點爲null,即作爲根節點,對應——“新根”的情況*/
        if ((xp = x.parent) == null) {
            //改變顏色即可
            x.red = false;
            return x;
        }
        /*2 如果父節點爲黑色,此時是天然平衡的不需要調整,||後面的操作僅僅是爲了給祖父節點賦值,對應——“父黑”的情況!*/
        else if (!xp.red || (xpp = xp.parent) == null)
            return root;
        /*3 剩下的就是父節點是紅色的情況,此時祖父節點肯定存在並且是黑色!這是插入之後最複雜的情況了,需要更加細緻的分類討論*/
        /* 如果父節點是作爲祖父節點的左子節點,即L,那麼叔節點肯定是右子節點*/
        if (xp == (xppl = xpp.left)) {
            /*3.1 如果叔節點爲紅色,很明顯,對應——“父紅叔紅”的情況*/
            /*此時可以將父/叔 (P/U) 節點塗黑,祖父節點(G)塗紅;而後以祖父節點(G)作爲新的平衡節點N,向上遞歸的執行平衡操作,
            直到不再發生兩個相連的紅色節點或者達到根(它將被重新塗成黑色)爲止。(摘自紅黑樹實現原理)*/
            if ((xppr = xpp.right) != null && xppr.red) {
                //叔節點塗黑
                xppr.red = false;
                //父節點塗黑
                xp.red = false;
                //祖父節點塗紅
                xpp.red = true;
                //祖父節點作爲新插入節點,遞歸操作直到平衡,這裏使用的for循環處理,思想很優秀!
                x = xpp;
            }
            /*3.2 如果叔節點爲黑色(或者不存在),很明顯,對應——“父紅叔黑”的情況,在原理部分我們討論過,需要分四種情況,但是由於父節點在前面確認屬於左邊,即L,因此有兩種情況:
             * 1)  在祖父節點G的左孩子節點P的左子樹中插入節點N,簡稱“LL”;
             * 2)  在祖父節點G的左孩子節點P的右子樹中插入節點N,簡稱“LR”;
             * */
            else {
                /*3.2.1 如果新插入節點,屬於父節點的右子節點,即對應——
                 * 2)  在祖父節點G的左孩子節點P的右子樹中插入節點N,簡稱“LR”;的情況
                 * 此時的處理方式是:先將P左旋,實際上是轉換爲LL的情況,然後將G右旋;然後N塗黑,G塗紅。HashMap的處理方法差不多,只是順序有變。
                 * */
                /*首先將LR轉換爲LL的情況。*/
                if (x == xp.right) {
                    //重新爲x賦值爲xp,這裏將P左旋,然後原xp節點成爲了原x節點的左子節點,實際上是轉換成了LL的情況
                    root = rotateLeft(root, x = xp);
                    //重新爲xp、xpp賦值
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                /*3.2.2 這裏實際上是處理LL的情況,有可能本來就是LL的情況,也有可能是LR轉換爲LL的情況,即
                 * 1)  在祖父節點G的左孩子節點P的左子樹中插入節點N,簡稱“LL”;
                 * 此時的處理方式是:將G右旋,然後P塗黑,G塗紅;
                 * */
                if (xp != null) {
                    //LL的父節點P塗黑,也對應着LR的N塗黑。
                    xp.red = false;
                    //如果祖父節點不爲null
                    if (xpp != null) {
                        //LL的祖父節點G塗紅,實際上對應着LR的G塗紅
                        xpp.red = true;
                        //最後將G右旋,到此實際上平衡完畢了,但是HashMap沒有主動結束,而是繼續循環,下一次循環時,將會變成“父黑”的情況,直接結束!
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        /* 如果父節點是作爲祖父節點的右子節點,即R,那麼叔節點肯定是右子節點*/
        else {
            /*3.1 如果叔節點爲紅色,很明顯,對應——“父紅叔紅”的情況*/
            /*此時可以將父/叔 (P/U) 節點塗黑,祖父節點(G)塗紅;而後以祖父節點(G)作爲新的平衡節點N,向上遞歸的執行平衡操作,
            直到不再發生兩個相連的紅色節點或者達到根(它將被重新塗成黑色)爲止。(摘自紅黑樹實現原理)*/
            if (xppl != null && xppl.red) {
                //叔節點塗黑
                xppl.red = false;
                //父節點塗黑
                xp.red = false;
                //祖父節點塗紅
                xpp.red = true;
                //祖父節點作爲新插入節點,遞歸操作直到平衡,這裏使用的for循環處理,思想很優秀!
                x = xpp;
            }
            /*3.2 如果叔節點爲黑色(或者不存在),很明顯,對應——“父紅叔黑”的情況,在原理部分我們討論過,需要分四種情況,但是由於父節點在前面確認屬於右邊,即R,因此有兩種情況:
             * 3)  在祖父節點G的右孩子節點P的左子樹中插入節點N,簡稱“RL”;
             * 4)  在祖父節點G的右孩子節點P的右子樹中插入節點N,簡稱“RR”。
             * */
            else {
                /*3.2.3 如果新插入節點,屬於父節點的左子節點,即對應——
                 * 3)  在祖父節點G的右孩子節點P的左子樹中插入節點N,簡稱“RL”;的情況
                 * 此時的處理方式是:先將P右旋,實際上是轉換爲RR的情況,然後將G左旋;然後N塗黑,G塗紅。HashMap的處理方法差不多,只是順序有變。
                 * */
                /*首先將RL轉換爲RR的情況。*/
                if (x == xp.left) {
                    //重新爲x賦值爲xp,這裏將P右旋,然後原xp節點成爲了原x節點的右子節點,實際上是轉換成了RR的情況
                    root = rotateRight(root, x = xp);
                    //重新爲xp、xpp賦值
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                /*3.2.4 這裏實際上是處理RR的情況,有可能本來就是RR的情況,也有可能是RL轉換爲RR的情況,即
                 * 4)  在祖父節點G的右孩子節點P的右子樹中插入節點N,簡稱“RR”。
                 * 此時的處理方式是:將G左旋,然後P塗黑,G塗紅;
                 * */
                if (xp != null) {
                    //RR的父節點P塗黑,也對應着RL的N塗黑。
                    xp.red = false;
                    //如果祖父節點不爲null
                    if (xpp != null) {
                        //RR的祖父節點G塗紅,實際上對應着RL的G塗紅
                        xpp.red = true;
                        //最後將G左旋,到此實際上平衡完畢了,但是HashMap沒有主動結束,而是繼續循環,下一次循環時,將會變成“父黑”的情況,直接結束!
                        root = rotateLeft(root, xpp);
                    }
                }
            }
        }
    }
}
5.2.4.1.2.1 rotateLeft左旋和rotateRight右旋方法

  rotateLeft是紅黑樹左旋的通用方法,用於調整右樹失衡的情況;rotateRight是紅黑樹右旋的通用方法,用於調整左樹失衡的情況。
  不同紅黑樹的實現雖然代碼可能不一樣,但是旋轉的原理的都是一樣的,紅黑樹和AVL樹的單純的旋轉原理又是一樣的,在AVL樹原理部分已經有了詳細講解,在此不再贅述:數據結構—平衡二叉樹(AVL樹)的原理以及Java代碼的完全實現

/**
 * 左旋的方法,和右旋是鏡像的,根據紅黑樹原理,左旋的通解是:
 * 設k1爲需要旋轉的節點,k2爲k1的右子節點,左旋之後,k2成爲根節點,k1成爲k2的左子節點,k2的左子樹2成爲k1的右子樹
 *
 * @param root 當前根節點
 * @param p    需要左旋的節點
 * @return 新的根節點
 */
static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root, TreeNode<K, V> p) {
    TreeNode<K, V> r, pp, rl;
    //如果p(k1)以及p的右子節點r(k2)不爲null
    if (p != null && (r = p.right) != null) {
        //如果r(k2)的左子樹rl不爲null
        //同時p(k1)的右子節點設置爲r(k2)的左子節點
        if ((rl = p.right = r.left) != null)
            //那麼rl的父節點變成成爲p(k1)
            rl.parent = p;
        //如果pp即p的父節點爲null,說明此時p(k1)是根節點,那麼旋轉之後r(k2)成爲根節點
        //同時r(k2)的父節點設置爲p(k1)的父節點
        if ((pp = r.parent = p.parent) == null)
            //r賦值給root,並且塗黑
            (root = r).red = false;
            //否則,如果p(k1)是pp的左子節點
        else if (pp.left == p)
            //那麼r(k2)成爲pp的左子節點,即代替了p的位置
            pp.left = r;
            //否則,r(k2)成爲pp的右子節點,即代替了p的位置
        else
            pp.right = r;
        //r(k2)的左子節點變成p(k1)
        r.left = p;
        //p(k1)的父節點變成r(k2)
        p.parent = r;
    }
    //返回新根節點
    return root;
}

/**
 * 右旋的方法,和左旋是鏡像的,根據紅黑樹原理,右旋的通解是:
 * 設k1爲需要旋轉的節點,k2爲k1的左子節點,右旋之後,k2成爲根節點,k1成爲k2的右子節點,k2的右子樹2成爲k1的左子樹
 *
 * @param root 當前根節點
 * @param p    需要右旋的節點
 * @return 新的根節點
 */
static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root, TreeNode<K, V> p) {
    TreeNode<K, V> l, pp, lr;
    //如果p(k1)以及p的左子節點l(k2)不爲null
    if (p != null && (l = p.left) != null) {
        //如果l(k2)的右子樹lr不爲null
        //同時p(k1)的左子節點設置爲l(k2)的右子節點
        if ((lr = p.left = l.right) != null)
            //那麼lr的父節點變成成爲p(k1)
            lr.parent = p;
        //如果pp即p的父節點爲null,說明此時p(k1)是根節點,那麼旋轉之後l(k2)成爲根節點
        //同時l(k2)的父節點設置爲p(k1)的父節點
        if ((pp = l.parent = p.parent) == null)
            //l賦值給root,並且塗黑
            (root = l).red = false;
            //否則,如果p(k1)是pp的右子節點
        else if (pp.right == p)
            //那麼l(k2)成爲pp的左子節點,即代替了p的位置
            pp.right = l;
            //否則,l(k2)成爲pp的左子節點,即代替了p的位置
        else
            pp.left = l;
        //l(k2)的左子節點變成p(k1)
        l.right = p;
        //p(k1)的父節點變成l(k2)
        p.parent = l;
    }
    return root;
}
5.2.4.1.3 調整root位置moveRootToFront方法

  moveRootToFront方法,被用在紅黑樹完全構建成功之後。
  將鏈表完全轉換爲紅黑樹之後,紅黑樹可能經歷了多次再平衡,需要將最後的root節點,設置爲數組節點,即tab[i]=root。
  另外,我們從treeifyBin方法的源碼中能看出來,實際上紅黑樹是通過紅黑樹鏈表構建而來的,而在構建紅黑樹時,鏈表節點之間的prev和next關係是沒有清除的,因此紅黑樹的節點之間還保持着鏈表的neext關係。因此還需要調整root的next和prev的引用指向,將root節點作爲鏈表的頭節點。

/**
 * moveRootToFront方法,被用在紅黑樹完全構建成功之後。
 * 將root節點作爲數組桶位節點以及鏈表頭節點
 */
static <K, V> void moveRootToFront(Node<K, V>[] tab, TreeNode<K, V> root) {
    int n;
    //非空判斷
    if (root != null && tab != null && (n = tab.length) > 0) {
        //計算root節點的槽位,這個槽位就是原紅黑樹鏈表的槽位
        int index = (n - 1) & root.hash;
        //獲取該位置的節點,這個節點實際上就是在treeifyBin方法中在調用treeify方法前設置的紅黑樹鏈表頭節點
        TreeNode<K, V> first = (TreeNode<K, V>) tab[index];
        /*判斷鏈表頭節點是否還作爲紅黑樹的根節點
         * 如果root和first不相等,那麼需要調整引用關係
         * 簡單來說就是將,紅黑樹根節點調整爲原鏈表的頭節點,並設置爲數組桶位置的節點,大概有三步:
         * 1 數組桶位的節點設置爲紅黑樹根節點root
         * 2 解除root節點和原紅黑樹鏈表節點之間的next和prev關係,root的前驅和後繼直接關聯
         * 3 重新關聯root節點和原紅黑樹鏈表的關係,將root設置爲鏈表頭節點
         *
         * */
        if (root != first) {
            Node<K, V> rn;
            /*1 數組桶位的節點設置爲紅黑樹根節點root*/
            tab[index] = root;

            /*2 解除root節點和原紅黑樹鏈表節點之間的next和prev關係,root的前驅和後繼直接關聯*/
            //獲取root的前驅節點rp
            TreeNode<K, V> rp = root.prev;
            //獲取root的後繼rn並判斷是否不爲null,不爲null就說明root節點不是原鏈表追後一個節點
            if ((rn = root.next) != null)
                //後繼節點rn的前驅節點設置爲rp
                ((TreeNode<K, V>) rn).prev = rp;
            //如果rp不爲null,那麼說明root不是原鏈表頭節點
            if (rp != null)
                //那麼前驅節點的後繼節點設置爲root的後繼節點rn
                rp.next = rn;
            //如果first不爲null
            if (first != null)
                //那麼原鏈表頭節點的前驅設置爲紅黑樹根節點root
                first.prev = root;

            /*3 重新關聯root節點和原紅黑樹鏈表節點的關係,將root設置爲鏈表頭節點*/
            //紅黑樹根節點root的後繼節點設置爲原鏈表頭節點first
            root.next = first;
            //紅黑樹根節點root的前驅設置爲null
            root.prev = null;
        }
        // 校驗該紅黑樹結構是否正確
        assert checkInvariants(root);
    }
}

5.2.4.2 樹形化的總結

  通過查看JDK1.8HashMap樹形化方法treeifyBin的源碼,我們能夠發現一些不爲人知的細節:

  1. 樹形化的要求: 在外面的方法判斷插入節點之後鏈表長度大於8並調用該方法時,在該方法裏面還會判斷當前哈希表中的容量大於等於MIN_TREEIFY_CAPACITY(即64) 時,才允許樹形化鏈表,否則不進行樹形化,而是擴容一次。
  2. 樹形化過程: 普通節點類型鏈表(節點具有next關係) --> 紅黑樹節點類型鏈表(節點具有prev和next關係) --> 紅黑樹(保留了節點的prev和next關係)
  3. 紅黑樹的根節點最終會作爲數組桶位的直達節點,並調整爲紅黑樹鏈表的頭節點。

  由此,我們能夠畫出HashMap樹形化的轉換流程圖:
  首先是,鏈表長度大於8,同時數組容量大於等於64的情況,此時可以轉換爲紅黑樹:
在這裏插入圖片描述
  然後是,轉換爲紅黑樹節點鏈表,此時全部是默認黑色節點:
在這裏插入圖片描述
  然後是,紅黑樹鏈表轉換爲紅黑樹之後,調整root位置/引用之前的結構,此時數組桶位節點還是原鏈表頭節點:
在這裏插入圖片描述
  最後是,調整root位置/引用之後的結構,root變成了鏈表頭節點,以及數組桶位節點。樹形化徹底完成:
在這裏插入圖片描述

5.2.5 插入紅黑樹節點putTreeVal方法

  putTreeVal方法主要有兩個作用,

  1. 一個是先查找是否具有相同的key的節點,找打就返回該節點,表明需要替換value;
  2. 沒找到那就是插入新節點了,然後返回null,表示插入了節點。

  key相同的的要求是:兩個key的hash相同,並且兩個key的equals或者==比較返回true。
  putTreeVal插入的邏輯和treeify由紅黑樹鏈表構建紅黑樹的方法有些相似,都有尋找位置-插入節點-調整平衡-調整root引用關係的步驟,理解了一個方法另一個方法也就不難理解了。

/**
 * 插入紅黑樹節點的方法,該方法有一部分和treeify樹形化方法相似
 * 該方法由紅黑樹節點調用
 *
 * @param map 當前map集合
 * @param tab 當前集合的table數組
 * @param h   key的hash
 * @param k   key
 * @param v   value
 * @return 返回需要替換value的樹節點,或者在新添加樹節點之後返回null
 */
final TreeNode<K, V> putTreeVal(HashMap<K, V> map, Node<K, V>[] tab, int h, K k, V v) {
    //存儲k的class對象
    Class<?> kc = null;
    // 標識是否已經遍歷過一次當前節點。 false表示沒有,true 表示已經遍歷過
    boolean searched = false;
    //判斷該方法調用節點的父節點是否爲null,來獲取根節點
    TreeNode<K, V> root = (parent != null) ? root() : this;
    //從根節點開始遍歷紅黑樹鏈表,尋找相等的節點
    for (TreeNode<K, V> p = root; ; ) {
        //dir表示尋找的方向 -1表示左邊 1表示右邊
        //ph用來存儲當前節點的hash
        int dir, ph;
        //用來存儲當前節點的key
        K pk;
        /*先比較兩個key的hash*/
        //如果當前節點hash值ph 大於 指定key的hash值h
        if ((ph = p.hash) > h)
            //那麼dir=-1 表示新節點應該放在左側
            dir = -1;
            //否則,如果當前節點hash值ph 小於 指定key的hash值h
        else if (ph < h)
            //那麼dir=1 表示新節點應該放在右側
            dir = 1;
            /*如果hash相等,然後使用key的equals方法比較*/
            //否則,如果兩個key使用==比較或者使用equals比較返回true,那說明存在相同的key,此時應該進行value的替換
            //返回當前節點p,該節點就是需要替換value的節點,這裏不進行替換,在putVal方法中進行統一替換
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
            /*如果equals方法比較返回false,然後嘗試將兩個key轉換爲Comparable進行比較*/
            //否則,通過上面的方法不能分出大小,即key的hash值相等,但是key不相等,那麼和treeify樹形化方法的比較操作相似
            //使用comparableClassFor和compareComparables方法進行比較,實際上是想將兩個key轉換爲Comparable進行比較
        else if ((kc == null &&
                (kc = comparableClassFor(k)) == null) ||
                (dir = compareComparables(kc, k, pk)) == 0) {
            /*如果上面的比較完畢還是到了這一步,說明兩個key無法比較(無法轉換爲Comparable),或者Comparable比較返回0*/
            /*判斷searched並嘗試搜索該節點的左子樹*/
            //標識是否已經遍歷過一次當前節點,如果爲true表示遍歷過,那麼它的子樹也肯定遍歷過
            //如果爲false,那麼它的子樹也肯定沒有遍歷過,因爲是節點的遍歷是從上向下遍歷的
            //那麼此時!searched就爲true,開始遍歷左右子樹
            if (!searched) {
                TreeNode<K, V> q, ch;
                //searched設置爲true
                searched = true;
                /*可以看到兩個表達式中間使用||連接,那麼是短路法運算。先是從左子樹中嘗試查找相等key的節點,如果找到了那麼就不找右子樹了
                如果沒找到,那麼第一個表達式返回false,那麼繼續查找右子樹,如果在其中一方找到了相同的key,那麼返回對應的節點q,
                該節點q就是需要替換value的節點,這裏不進行替換,在putVal方法中進行統一替換*/
                if (((ch = p.left) != null &&
                        //查詢左子樹
                        (q = ch.find(h, k, kc)) != null) ||
                        ((ch = p.right) != null &&
                                //查詢右子樹
                                (q = ch.find(h, k, kc)) != null)) {

                    return q;
                }
            }
            /*如果上面的遍歷了所有子節點也沒有找到和當前鍵equals相等的節點,那麼使用tieBreakOrder方法進行最終的比較,
             * 但是這個方法只會返回1或者-1,實際上這已經是在爲插入新節點做準備了
             * */
            dir = tieBreakOrder(k, pk);
        }
        /*到這一步,表示該節點以及它的所有子節點都沒找到相同的key,那麼需要插入新節點*/
        // xp保存當前節點
        TreeNode<K, V> xp = p;
        /*這裏就和treeify樹形化方法相似了,都是插入節點,這裏摘取treeify部分的描述*/
        //如果dir小於等於0,那麼x節點一定在當前樹節點p的左側;否則,如果dir 大於0,那麼x節點一定在當前樹節點p的右側
        //由於僅僅知道x是在p的左側或者右側,不知道p是否有左、右子樹,如果已經存在了左右子樹,那麼還需要遞歸子樹,直到查找到對應位置爲null,因此還需要判斷:
        //如果x是在p的左側,並且當p的左子樹爲null時,直接使x成爲xp的左子節點,x.parent指向xp,然後xp.left指向x
        //如果x是在p的右側,並且當p的右子樹爲null時,直接使x成爲xp的右子節點,x.parent指向xp,然後xp.right指向x
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            /*如果某個子節點爲null,那麼在該位置插入新節點作爲xp的子節點*/
            //獲取xp的的next節點xpn
            Node<K, V> xpn = xp.next;
            //新建紅黑樹節點x,該節點的next指向xpn
            TreeNode<K, V> x = map.newTreeNode(h, k, v, xpn);
            //成爲xp的左子節點
            if (dir <= 0)
                xp.left = x;
                //成爲xp的右子節點
            else
                xp.right = x;
            /*這裏是相比於treeify方法多出的部分,由於treeify方法本身就是操作的紅黑樹鏈表,他們的節點天然具有next和prev關係
             * 這裏插入新的節點和目前的紅黑樹節點不具有next和prev關係,因此需要維持關係
             * 這裏關係的維持方式是:x的父節點xp作爲前驅prev,x作爲父節點xp的後繼next
             * 上面新建節點時則維護了父節點xp的next節點xpn作爲新節點x的後繼next,這裏繼續判斷如果xpn != null,新節點x作爲父節點xp的next節點xpn的前驅prev
             * 總結來說:新插入節點 在紅黑樹連變化中的位置,是插入到它的父節點和父節點的next節點之間!
             * */
            //xp的next指向新插入的節點x
            xp.next = x;
            //新插入節點x的父節點和前驅節點都指向xp
            x.parent = x.prev = xp;
            //如果xpn不爲null
            if (xpn != null)
                //那麼xpn的前驅指向新節點x
                ((TreeNode<K, V>) xpn).prev = x;
            //同樣插入節點之後需要balanceInsertion調整平衡,以及moveRootToFront重設root的引用關係
            //這兩個方法在"構建紅黑樹treeify方法"部分已經詳細講解了,在此不作贅述
            moveRootToFront(tab, balanceInsertion(root, x));
            //返回null,表示新插入了節點
            return null;
        }
    }
}

/**
 * 獲取根節點的方法root()
 */
final TreeNode<K, V> root() {
    //從當前節點向上尋找,直到某個節點的parent爲null,那麼該節點就是根節點
    for (TreeNode<K, V> r = this, p; ; ) {
        if ((p = r.parent) == null)
            return r;
        r = p;
    }
}

/**
 * 新建紅黑樹節點
 *
 * @param hash  k的hash
 * @param key   k
 * @param value v
 * @param next  next
 * @return 紅黑樹節點
 */
TreeNode<K, V> newTreeNode(int hash, K key, V value, Node<K, V> next) {
    return new TreeNode<>(hash, key, value, next);
}

5.2.5.1 find查找相同節點的方法

  find方法用於根據指定key查找key相同的節點。查找成功就返回找到的節點,查找失敗就返回null。這個查找實際上就是二叉排序樹的查找遞歸查找。在插入獲取和刪除時都需要調用find方法。
  key相同的的要求是:兩個key的hash相同,並且兩個key的equals或者==比較返回true。

/**
 * 以調用該方法的該節點作爲根節點,查找其所有子孫節點,嘗試匹配給定的h或者k或者kc
 * 就是二叉排序樹的查找方式,遞歸查找
 *
 * @param h  k的hash
 * @param k  key
 * @param kc k的類型
 * @return 沒找到就返回null, 找到匹配的就返回該節點
 */
final TreeNode<K, V> find(int h, Object k, Class<?> kc) {
    //獲取調用節點
    TreeNode<K, V> p = this;
    /*循環該節點的子樹查找相等的key*/
    do {
        //dir表示尋找的方向 -1表示左邊 1表示右邊
        //ph用來存儲當前節點的hash
        int ph, dir;
        //用來存儲當前節點的key
        K pk;
        //獲取p的左子結點pl和右子節點pr
        TreeNode<K, V> pl = p.left, pr = p.right, q;
        /*首先比較hash值*/
        //如果當前節點的hash值大於k的hash值h,那麼應該查找左子樹
        if ((ph = p.hash) > h)
            p = pl;
            //如果當前節點的hash值小於k的hash值h,那麼應該查找右子樹
        else if (ph < h)
            p = pr;
            /*到這裏,說明hash值相等,那麼比較equals或者==*/
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            //到這裏還是相等,那麼就算找到了相等的key的節點,返回p
            return p;
            /*到這裏,說明hash相等但是比較equals或者==都返回false,那麼嘗試超找左/右子節點,前提是必須有一個子節點爲null*/
            //如果左子節點爲null,那麼查找右子節點
        else if (pl == null)
            p = pr;
            //如果右子節點爲null,那麼查找左子節點
        else if (pr == null)
            p = pl;
            /*如果左右孩子都不爲空,嘗試將兩個key轉換爲Comparable進行比較,來確定到底該往哪個方向去對比*/
        else if ((kc != null ||
                (kc = comparableClassFor(k)) != null) &&
                (dir = compareComparables(kc, k, pk)) != 0)
            /*到這一步,說明兩個key可以比較(可以轉換爲Comparable),或者Comparable比較不返回0*/
            //dir小於0,就查找左子樹,否則查找右子樹。
            p = (dir < 0) ? pl : pr;
            /*到這一步,說明兩個key無法比較(不可以轉換爲Comparable),或者Comparable比較返回0
             * 那麼指定從右子樹遞歸查找
             * */
        else if ((q = pr.find(h, k, kc)) != null)
            //如果返回q不爲null,說明找到了,那麼返回q
            return q;
            /*如果從右子樹遞歸查找後仍未找到,那麼從左子樹開始循環查找*/
        else
            p = pl;
        /*如果p爲null,表示查找到了葉子節點,那麼循環結束*/
    } while (p != null);
    /*循環結束還是沒找到,返回null*/
    return null;
}

5.3. 總結

5.3.1 put方法的關鍵流程圖

  根據上面的規律,我們可以總結出JDK1.8的HashMap的put方法的關鍵流程圖:
在這裏插入圖片描述

5.3.2 resize方法關鍵流程圖

  JDK1.8HashMap的resize方法(用於初始化和擴容)的關鍵流程圖:
在這裏插入圖片描述

6 remove方法

public V remove(Object key)

  從此map中移除指定鍵的鍵值對(如果存在)。返回與 key 關聯的value;如果沒有指定key,則返回 null。(返回 null 還可能表示該映射之前將 null 與 key 關聯。)
  remove方法是HashMap的核心方法之一,源碼較多,主要難點在於移除紅黑樹節點之後的平衡方法,不過這些都是有章可循的!
  remove方法可以分爲兩步:

  1. 尋找與給定key相同的節點;
  2. 找到了就移除該節點,返回該節點的value;沒找到就返回null
/**
 * 移除節點開放給外部調用的方法,根據指定key移除找到的節點
 * 1)  尋找與給定key相同的節點;
 * 2)  找到了就移除該節點,返回該節點的value;沒找到就返回null
 *
 * @param key k
 * @return 返回與 key 關聯的value;如果沒有指定key,則返回 null。(返回 null 還可能表示該映射之前將 null 與 key 關聯。)
 */
public V remove(Object key) {
    Node<K, V> e;
    //主要是調用removeNode方法,hash(key)方法在最前面添加節點時就已經分析了
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
}

6.1 removeNode移除節點的總方法

  removeNode是移除節點的總方法,即很多開放給外部調用的API中,內部就是調用的該方法。可以是根據key移除節點,也可以是根據key和value移除節點。
  大概可以分爲如下幾步:

  1. 在哈希表中嘗試查找與key相同的節點;
  2. 如果找到了節點,那麼判斷是否符合指定的模式:是根據key移除還是根據key和value移除,如果符合,那麼嘗試移除節點,並返回被移除的節點,否則返回null。
/**
 * 移除節點的總方法,可以是根據key移除,也可以是根據key和value移除
 *
 * @param hash       key的hash
 * @param key        要匹配的key
 * @param value      要匹配的value
 * @param matchValue 如果爲 true,則需要在鍵和值 都比較並相等時才刪除;否則只比較key
 * @param movable    如果爲 false,則在刪除時不移動其他節點,用在紅黑樹中
 * @return 返回被刪除的節點,沒有刪除則返回null
 */
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;
    /*判斷哈希表是否非空(不爲null且有節點),以及key通過哈希算法計算出來的桶爲是否具有節點*/
    //如果table非空,key對應桶位節點p不爲null,那麼才進一步處理,否則直接返回null
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
        //node保存要返回的節點
        Node<K, V> node = null, e;
        K k;
        V v;
        /*1 在哈希表中查找與key相同的節點*/
        //如果key和數組桶爲的第一個節點就像等了,那麼node賦值爲p
        //相等的條件是:兩個key的hash相同,並且兩個key的equals或者==比較返回true。
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
            /*否則,查找該位置的鏈表或者紅黑樹*/
        else if ((e = p.next) != null) {
            /*如果p是紅黑樹節點類型,那麼p調用getTreeNode方法查找,
             * 相等的條件是:兩個key的hash相同,並且兩個key的equals或者==比較返回true。*/
            if (p instanceof TreeNode)
                //getTreeNode方法下面有詳解
                node = ((TreeNode<K, V>) p).getTreeNode(hash, key);
                /*否則,就是普通節點類型,那麼遍歷鏈表查找*/
            else {
                /*do while循環鏈表*/
                do {
                    //相等的條件是:兩個key的hash相同,並且兩個key的equals或者==比較返回true。
                    if (e.hash == hash &&
                            ((k = e.key) == key ||
                                    (key != null && key.equals(k)))) {
                        //如果key相等,那麼node賦值爲e,結束循環
                        node = e;
                        //這裏break之後,下面的p = e;將不會執行,那麼e就是p的next節點,即node是p的next節點
                        break;
                    }
                    //p=e
                    p = e;
                    //e=e.next
                } while ((e = e.next) != null);
            }
        }
        /*2 嘗試移除節點
         * &&左邊的表達式一判斷bode是否不爲null,不爲null就表示找到了key相等的節點
         * &&右邊的表達式二然後根據matchValue的值繼續判斷,如果matchValue爲false,那麼由於短路法後面的表達式爲true
         * 如果matchValue爲true,那麼比較value是否相等,相等的條件是:兩個value的equals或者==比較返回true。
         * &&兩邊的表達式都爲true時,進入if方法體,否則返回null
         * */
        if (node != null && (!matchValue || (v = node.value) == value ||
                (value != null && value.equals(v)))) {
            /*2.1 如果找到的節點node屬於紅黑樹節點,那麼node節點調用removeTreeNode方法移除節點*/
            if (node instanceof TreeNode)
                ((TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
                /*2.2 否則,如果找到的節點node等於p,即是數組節點,那麼直接將數組桶位指向該節點的next節點即可*/
            else if (node == p)
                tab[index] = node.next;
                /*2.3 否則,由於在上面鏈表中查找的時候,最終node是p的next節點
                 * 那麼p.next指向node.next,即將node去除
                 * */
            else
                p.next = node.next;
            //數據結構改變次數自增1
            ++modCount;
            //節點數量自減1
            --size;
            //元素被刪除之後的回調方法,該方法在HashMap中的實現爲空
            //是留給子類LinkedHashMap實現的,用於刪除LinkedHashMap中維護的雙鏈表
            afterNodeRemoval(node);
            //返回node
            return node;
        }
    }
    //返回null
    return null;
}

/**
 * 紅黑樹節點內部的方法,由紅黑樹節點調用,用於獲取和指定key相等的節點
 *
 * @param h k的hash
 * @param k k
 * @return 查找到的節點,沒找到就返回null
 */
final TreeNode<K, V> getTreeNode(int h, Object k) {
    //首先是判斷當前節點是否根節點,即父節點是否爲null,如果不是那麼root()方法獲取根節點,root方法在“插入紅黑樹節點putTreeVal方法”有詳解
    //然後根節點用調用find方法超找指定key的鍵值對,就是二叉排序樹的查找方法,find方法在“find查找相同節點的方法”部分有詳解
    return ((parent != null) ? root() : this).find(h, k, null);
}

  下面對重要方法單獨講解!

6.1.1 removeTreeNode移除紅黑樹節點的方法

  該方法僅僅被用在刪除節點的removeNode方法中,用於移除紅黑樹的某個節點。
  如果我們對紅黑樹有所瞭解,那麼我們會知道,紅黑樹最複雜的地方就是刪除節點的方法了,因爲有很多種情況需要考慮到,需要對移除節點的紅黑樹進行再平衡操作。不同的代碼雖然實現不同,但是原理基本上一致,如果懂得了原理,那麼看代碼就很簡單了:數據結構—紅黑樹(RedBlackTree)的實現原理以及Java代碼的完全實現
  大概分爲如下幾步:

  1. 將節點與紅黑樹鏈表的關係移除(next、prev關係);
  2. 判斷紅黑樹是否符合某些結構,如果符合那麼將紅黑樹轉換爲鏈表,方法結束;
  3. 如果不符合,那麼先通過二叉排序樹的方式查找需要被真正移除的節點位置,找到之後如果不是原位置那麼交換需要移除的節點和真正需要被移除的節點的數據(right、left、parent關係);
  4. 如果需要移除的節點具有子節點(真正需要移除的位置的節點要麼沒有子節點要麼只有一個子節點)。那麼先使用二叉排序樹的移除方法將需要移除的節點移除了(right、left、parent關係);
  5. 調整平衡。這裏的調整把需要移除的節點沒有子節點的情況都考慮進去了,並返回調整平衡之後的root根節點;
  6. 在調整平衡之後,如果需要移除的節點沒有子節點,那麼將需要移除的節點移出了(right、left、parent關係);
  7. 由於可能調整了樹節點,導致產生新的root節點,因此調用moveRootToFront方法將root節點作爲數組桶位節點以及鏈表頭節點。
/**
 * 該方法由紅黑樹節點對象調用
 * 移除紅黑樹節點,在移除鏈表關係之後移除節點關係之前如果符合某些紅黑樹結構,紅黑樹將轉換爲鏈表。
 *
 * @param map     當前map
 * @param tab     當前table數組
 * @param movable 如果爲 false,則在刪除時不移動其他節點
 */
final void removeTreeNode(HashMap<K, V> map, Node<K, V>[] tab, boolean movable) {
    int n;
    /*如果table數組爲null或者沒有節點,那麼直接返回*/
    if (tab == null || (n = tab.length) == 0)
        return;
    //獲取當前調用節點所在的數組桶位
    int index = (n - 1) & hash;
    //獲取數組節點 作爲鏈表頭節點first,同時作爲紅黑樹根節點root,rl作爲根節點的左節點
    TreeNode<K, V> first = (TreeNode<K, V>) tab[index], root = first, rl;
    //succ作爲當前調用節點的next節點,pred作爲當前調用節點的prev節點
    TreeNode<K, V> succ = (TreeNode<K, V>) next, pred = prev;

    /*1
     * 我們前面講過,HashMap中的紅黑樹的節點還通過next和prev維持雙向鏈表的特徵
     * 因此下面的代碼 是將當前this節點從紅黑樹鏈表(next和prev)中移除
     * */
    /*1.1 如果前驅節點爲null,那說明當前節點是根節點,並且是鏈表頭節點
     * 如果前驅節點不爲null,那說明當前節點不是根節點,並且不是鏈表頭節點
     * */
    if (pred == null)
        //那麼數組位置的節點使用當前調用節點的next節點來代替
        //並且first當前節點的next節點succ
        tab[index] = first = succ;
    else
        //那麼前驅節點pred的next節點指向當前節點的next節點succ
        pred.next = succ;
    /*1.2 如果當前調用節點的next節點succ不爲null*/
    if (succ != null)
        //那麼succ的前驅節點設置爲pred
        succ.prev = pred;

    /*如果first爲null,即那麼直接返回正常情況下應該不會發生*/
    if (first == null)
        return;
    /*如果root的父節點不爲null,那麼獲取真正的root節點並賦值給root,正常情況下應該不會發生*/
    if (root.parent != null)
        root = root.root();
    /* 2 在刪除紅黑樹節點之前,判斷紅黑樹的結構,如果符合以下結構,那麼將紅黑樹轉換爲普通鏈表,然後方法結束
     *
     * 如果root爲null,或者root的右子樹爲null,或者root的左子樹rl爲null,或者左子樹rl的左子樹爲null
     * 出現上述情況的一種,那麼就表示紅黑樹節點太少了並且結構,將會使用untreeify方法將紅黑樹轉換爲鏈表
     * 注意這裏並不是判斷數量小於等於UNTREEIFY_THRESHOLD(6),而是判斷樹結構,
     * 從紅黑樹的結構,我們可以知道,最少節點爲3個時將會從紅黑樹轉換爲鏈表,最多可以在還有10個節點時即可轉換爲普通鏈表
     *
     * */
    if (root == null || root.right == null ||
            (rl = root.left) == null || rl.left == null) {
        //untreeify方法將紅黑樹轉換爲鏈表,轉換爲鏈表時實際上是使用到了的next和prev引用
        //當前節點的next和prev引用在上面就已經和紅黑樹鏈表脫離了關係,因此轉換爲鏈表之後,該節點自動丟失,因此直接返回即可
        tab[index] = first.untreeify(map);  // too small
        return;
    }
    /*3 走到這裏 開始刪除紅黑樹節點*/
    //p爲當前待刪除節點 pl爲左子節點 pr爲右子節點,replacement用於記錄後續填充被刪除節點p的位置的節點
    TreeNode<K, V> p = this, pl = left, pr = right, replacement;
    /*3.1 如果左\右子節點都不爲null,那麼p不是葉子節點,這裏需要查找真正應該被刪除的節點
     * HashMap的查找方式和二叉排序樹的方式是一樣的,並且是尋找右子樹的最左(小)節點,替代目標節點,用來被刪除
     * 這裏找到之後會進行節點的替換,注意這裏的替換和某些紅黑樹的替換不一樣,
     * 這裏的替換包括顏色的替換和相關引用的替換,即“真正”的將節點P替換到需要被刪除的位置上,同時顏色還是保持了原來位置上的顏色
     * */
    if (pl != null && pr != null) {
        //s作爲真正需要被刪除的節點,目前賦值爲當前待刪除節點的右子節點;sl作爲s的左子節點
        TreeNode<K, V> s = pr, sl;
        /*尋找真正應該被刪除的節點s,實際上是二叉排序樹的刪除邏輯,我們知道有兩種查找方法:
         * 1 尋找右子樹的最左(小)節點,替代目標節點,用來被刪除
         * 2 尋找左子樹的最右(大)節點,替代目標節點,用來被刪除
         * 這裏JDK1.8的HashMap採用的是第一種
         * */
        while ((sl = s.left) != null) // find successor
            //s賦值爲sl
            s = sl;
        /*首先 交換s和p的顏色*/
        boolean c = s.red;
        s.red = p.red;
        p.red = c; // swap colors
        /*然後交換節點,所以說實際上是:最終節點換了位置,但是原位置上的節點顏色並沒有換*/
        //sr賦值爲最左(小)節點s的右子節點
        TreeNode<K, V> sr = s.right;
        //pp賦值爲待刪除節點p的父節點
        TreeNode<K, V> pp = p.parent;
        /*如果右子樹的最左(小)節點s等於待刪除節點的右子節點pr
         * 那說明待刪除節點p的右子節點pr沒有左子節點,pr就是右子樹的最左(小)節點s
         * 並且還說明 說明p是s的直接父節點
         * */
        if (s == pr) { // p was s's direct parent
            /*交換它們的紅黑樹部分引用父子關係*/
            p.parent = s;
            s.right = p;
        }
        /*否則,那說明待刪除節點p的右子節點pr具有左子節點,pr不是右子樹的最左(小)節點s
         * 並且還說明 說明p不是s的直接父節點
         * */
        else {
            /*交換它們的部分引用父子關係*/
            //獲取s的父節點sp
            TreeNode<K, V> sp = s.parent;
            //p的父節點指向sp並且不爲null
            if ((p.parent = sp) != nu | ll) {
                //那麼p爲sp的某個子節點
                if (s == sp.left)
                    sp.left = p;
                else
                    sp.right = p;
            }
            //pr作爲s的右子節點
            if ((s.right = pr) != null)
                pr.parent = s;
        }
        /*爲其他引用關係賦值*/
        //p被交換到右子樹的最左(小)節點上,那麼p.left肯定置爲null
        p.left = null;
        //p.right設置爲sr
        if ((p.right = sr) != null)
            sr.parent = p;
        //s.left 設置爲 pl
        if ((s.left = pl) != null)
            pl.parent = s;
        //如果父節點pp爲null,那麼s作爲根節點
        if ((s.parent = pp) == null)
            root = s;
            //否則,建立它們的引用關係
        else if (p == pp.left)
            pp.left = s;
        else
            pp.right = s;
        /*交換位置關係之後,s到了p的位置,p到了s的位置,即此時p作爲真正需要被刪除的節點*/
        /*尋找刪除p之後替代p位置的節點*/
        //交換關係之後,sr已被作爲p的右子節點,sr如果不爲null,那麼sr將代替p刪除後的位置
        if (sr != null)
            replacement = sr;
            //否則就是設置p本身代替p,實際上是此時的P沒有了子節點。
        else
            replacement = p;
    }
    /*3.2 如果左子樹pl不爲null,那麼pl將代替p刪除後的位置*/
    else if (pl != null)
        replacement = pl;
        /*3.3 如果右子樹pr不爲null,那麼pr將代替p刪除後的位置*/
    else if (pr != null)
        replacement = pr;
        /*3.4 否則,兩個子節點都爲null,那麼就是設置p本身代替p*/
    else
        replacement = p;

    /*
     * 4 如果replacement不等於p,那麼表示將由節點replacement代替p的位置;
     * 這裏先把p進行刪除,然後再進行平衡調整
     *
     * */
    if (replacement != p) {
        //replacement的父節點指向p的父節點
        TreeNode<K, V> pp = replacement.parent = p.parent;
        //父節點pp爲null,表示p爲根節點,刪除p之後則replacement變成根節點
        if (pp == null)
            root = replacement;
            //設置與父節點的左右孩子關係
        else if (p == pp.left)
            pp.left = replacement;
        else
            pp.right = replacement;
        //p的關聯引用置空
        p.left = p.right = p.parent = null;
    }

    /*
     * 5 判斷被刪除的p是否是紅色節點,如果是紅色,即屬於“刪紅”的情況,那麼不需要調整樹結構,直接刪除節點即可;
     * 否則需要balanceDeletion方法分情況重新平衡紅黑樹,並返回平衡後舊的新root節點
     * 這裏平衡的時候把replacement == p的情況也進行平衡了,因爲實際上這種情況可以看作“刪黑子黑的情況”,這裏把null節點看成黑色
     * */
    TreeNode<K, V> r = p.red ? root : balanceDeletion(root, replacement);

    /*6 如果replacement等於p,那麼表示沒有節點代替p的位置,有可能是sr爲null或者被刪除的節點P本來就沒有左右子節點,實際上都是p作爲葉子節點;
     * 也把p進行刪除,注意在此之前先進行了平衡調整,因此移除之後不必再進行平衡調整了。
     * */
    if (replacement == p) {  // detach
        /*下面的代碼將p的父節點pp和p的紅黑樹關係解除*/
        TreeNode<K, V> pp = p.parent;
        p.parent = null;
        if (pp != null) {
            if (p == pp.left)
                pp.left = null;
            else if (p == pp.right)
                pp.right = null;
        }
    }
    /*
     * 7 moveRootToFront用於將最後的root節點,設置爲數組節點,即tab[i]=root。
     * 同時調整root的next和prev的引用指向,將root節點作爲鏈表的頭節點。該方法源碼在添加元素時講過了
     * */
    if (movable)
        moveRootToFront(tab, r);
}

  其中某些方法比如moveRootToFront在前面添加元素時源碼已經講過,下面主要針對移除元素後調整平衡的方法進行講解!

6.1.1.1 移除後調整平衡balanceDeletion方法

  該方法比較複雜,因爲紅黑樹最難的操作就是移除節點後的平衡,情況非常多,但是HashMap這裏的實現代碼還是比較少的,因此可能會有很多巧妙地思想難以理解!需要對紅黑樹的原理有深入掌握才能更方便理解!

/**
 * 刪除節點時的平衡操作,進入該方法的情況是,被刪除的節點必須是黑色。
 * 該方法將一些複雜的情況最終轉換爲一些簡單的情況統一返回,非常的巧妙
 *
 * @param root 根節點
 * @param x    當前節點p或者替代p的replacement
 * @return 平衡止之後的根節點
 */
static <K, V> TreeNode<K, V> balanceDeletion(TreeNode<K, V> root,
                                             TreeNode<K, V> x) {
    for (TreeNode<K, V> xp, xpl, xpr; ; ) {
        /*1 如果x爲空,或者x爲根節點那麼直接返回root。下面的複雜情況處理完畢在下次循環時會成爲該情況*/
        if (x == null || x == root)
            //返回根節點
            return root;
            /*2 如果被刪除節點x的父節點xp爲null,那麼x就是根節點,將x染黑即可,返回x。下面的複雜情況處理完畢在下次循環時會成爲該情況。*/
        else if ((xp = x.parent) == null) {
            x.red = false;
            return x;
        }
        /*3 否則,如果x爲紅色,那麼屬於“刪黑子紅”,此時x塗黑即可,返回root。下面的複雜情況處理完畢在下次循環時會成爲該情況。*/
        else if (x.red) {
            /*x改爲黑色,返回root*/
            x.red = false;
            return root;
        }
        /*4 否則,屬於“刪子黑”,這並沒有考慮被刪除節點是紅色還是黑色的情況,因爲如果刪除節點是紅色,那麼子節點肯定是黑色,刪除紅色節點是不需要調整的;
         * 那麼即使對它進行調整的運用,也一定完全兼容被刪除節點是黑色的調整方式,思想很好!
         * */
        /*獲取獲取x的左兄弟節點xpl,如果xpl等於x,那麼說明x就是左子節點,兄弟節點就是右子節點*/
        else if ((xpl = xp.left) == x) {
            /*4.1  獲取右兄弟節點xpr,如果是紅色,屬於——“刪兄紅-右兄弟”
             * 處理方法是:則以父節點P爲基點左旋,然後B塗黑、P塗紅,最後將BL看成新的兄弟節點newB,轉換爲“刪兄黑”,然後統一處理。
             * */
            if ((xpr = xp.right) != null && xpr.red) {
                //右兄弟xpr塗黑
                xpr.red = false;
                //父節點xp塗紅
                xp.red = true;
                //父節點左旋
                root = rotateLeft(root, xp);
                //將BL看成新的兄弟節點newB
                xpr = (xp = x.parent) == null ? null : xp.right;
            }
            /*4.2 下面的就是“刪兄黑”的情,況如果xpr爲null,null也看成黑色節點
             * “刪兄黑”的情況是最複雜的一種情況
             * */
            /*4.2.1 如果xpr爲null,即“刪兄黑——兄子全黑”,null看成黑色*/
            if (xpr == null)
                //父節點xp當作新的x節點,繼續下一次循環,直到
                x = xp;
            else {
                //sl作爲右兄弟節點的左子節點,sr作爲右兄弟節點的右子節點
                TreeNode<K, V> sl = xpr.left, sr = xpr.right;
                /*如果(sr爲null或者sr爲黑色),並且(sl爲null或者sl爲黑色),即“刪兄黑——兄子全黑”,null看成黑色*/
                if ((sr == null || !sr.red) &&
                        (sl == null || !sl.red)) {
                    //兄弟節點xpr塗紅
                    xpr.red = true;
                    //父節點xp當作新的x節點
                    x = xp;
                }
                /* 上面的代碼處理了“兄子全黑-父黑、父紅“這兩種兩種情況,此時父節點xp作爲x,可能爲紅或者黑。”
                 * 傳統處理方法是:
                 * 對於兄子全黑-父黑:將兄弟節點B塗紅,將父節點P設爲新的C節點,將U設爲新B節點,將G設爲新P節點,回到刪黑子黑的情況,即向上遞歸進行處理,直到C成爲根節點或者達到平衡。
                 * 對於兄子全黑-父紅:將兄弟節點B塗紅,父節點P塗黑即可。此時原本刪除黑色節點的路徑補充了黑色節點,即可達到平衡。
                 *
                 * HashMap的處理方法是: 兄弟節點B塗紅,將父節點P設爲新的C節點向上遞歸進行處理,直到C成爲根節點或者達到平衡。
                 * HashMap沒有特意區分這兩種情況,而是統一循環處理了,減少了代碼量:
                 * 如果父節點xp是紅色,那麼在下一次向上循環時,將在情況3的地方塗黑並返回
                 * 如果父節點xp是黑色,那麼不斷向上循環,直到C成爲根節點或者達到平衡。
                 * */
                /*4.2.2 否則,表示屬於兄子非全黑的情況,需要進一步處理,此時可能是“兄右,右子黑或者兄右,右子紅”的情況
                 *
                 * */
                else {
                    /*如果右兄弟節點的右子節點爲null或者爲黑色,那麼屬於“兄右,右子黑”
                     * 處理方法是:以兄弟節點B爲基點右旋,然後BL塗黑,B塗紅,然後將BL當成新B,B當成新BR,這樣就轉換成了情況2-“兄右,右子紅”。
                     * */
                    if (sr == null || !sr.red) {
                        //右兄弟節點的左子節點sl(BL)塗黑,
                        if (sl != null)
                            sl.red = false;
                        //兄弟節點xpr(B)塗紅
                        xpr.red = true;
                        //以兄弟節點xpr(B)爲基點右旋
                        root = rotateRight(root, xpr);
                        //然後將BL當成新B,B當成新BR,這樣就轉換成了情況2-“兄右,右子紅”。
                        //實際上旋轉之後BL成爲了xp的right右子節點
                        xpr = (xp = x.parent) == null ?
                                null : xp.right;
                    }
                    /*下面就是“兄右,右子紅”的情況
                     * 處理方法是:以父節點P爲基點左旋,然後交換P和B的顏色(實際上就是兄弟節點的顏色設置爲父節點的顏色,父節點塗黑,因爲兄弟節點肯定是黑色的)
                     * ,然後BR塗黑,平衡完畢!
                     * */
                    if (xpr != null) {
                        //兄弟節點的顏色設置爲父節點的顏色
                        xpr.red = (xp == null) ? false : xp.red;
                        //sr(BR)塗黑
                        if ((sr = xpr.right) != null)
                            sr.red = false;
                    }
                    if (xp != null) {
                        //父節點塗黑
                        xp.red = false;
                        //以父節點xp(P)爲基點左旋
                        root = rotateLeft(root, xp);
                    }
                    /*設置x等於root,那麼將在下一次循環時直接在情況1中退出*/
                    x = root;
                }
            }
        }
        /*否則,那麼說明x就是右子節點,兄弟節點就是左子節點。它的處理方式和上面的if代碼塊中的方式是鏡像的*/
        else { // symmetric
            /*4.3  左兄弟xpl,如果是紅色,屬於——“刪兄紅-左兄弟”
             * 處理方法是:則以父節點P爲基點右旋,然後B塗黑、P塗紅,最後將BR看成新的兄弟節點newB,轉換爲“刪兄黑”,然後統一處理。
             * */
            if (xpl != null && xpl.red) {
                //左兄弟xpl(B)塗黑
                xpl.red = false;
                //父節點xp(p)塗紅
                xp.red = true;
                //以父節點xp(P)爲基點右旋
                root = rotateRight(root, xp);
                //將BR看成新的兄弟節點newB
                xpl = (xp = x.parent) == null ? null : xp.left;
            }
            /*4.4 下面的就是“刪兄黑”的情況,如果xpl爲null,null也看成黑色節點
             * “刪兄黑”的情況是最複雜的一種情況
             * */
            /*4.4.1 如果xpl爲null,即“刪兄黑——兄子全黑”,null看成黑色*/
            if (xpl == null)
                //父節點xp當作新的x節點
                x = xp;
            else {
                //sl作爲左兄弟節點的左子節點,sr作爲左兄弟節點的右子節點
                TreeNode<K, V> sl = xpl.left, sr = xpl.right;
                /*如果(sr爲null或者sr爲黑色),並且(sl爲null或者sl爲黑色),即“刪兄黑——兄子全黑”,null看成黑色*/
                if ((sl == null || !sl.red) &&
                        (sr == null || !sr.red)) {
                    //兄弟節點xpr塗紅
                    xpl.red = true;
                    //父節點xp當作新的x節點
                    x = xp;
                }
                /* 上面的代碼處理了“兄子全黑-父黑、父紅“這兩種兩種情況,此時父節點xp作爲x,可能爲紅或者黑。”
                 * 傳統處理方法是:
                 * 對於兄子全黑-父黑:將兄弟節點B塗紅,將父節點P設爲新的C節點,將U設爲新B節點,將G設爲新P節點,回到刪黑子黑的情況,即向上遞歸進行處理,直到C成爲根節點或者達到平衡。
                 * 對於兄子全黑-父紅:將兄弟節點B塗紅,父節點P塗黑即可。此時原本刪除黑色節點的路徑補充了黑色節點,即可達到平衡。
                 *
                 * HashMap的處理方法是: 兄弟節點B塗紅,將父節點P設爲新的C節點向上遞歸進行處理,直到C成爲根節點或者達到平衡。
                 * HashMap沒有特意區分這兩種情況,而是統一循環處理了,減少了代碼量:
                 * 如果父節點xp是紅色,那麼在下一次向上循環時,將在情況3的地方塗黑並返回
                 * 如果父節點xp是黑色,那麼不斷向上循環,直到C成爲根節點或者達到平衡。
                 * */
                /*4.4.2 否則,表示屬於兄子非全黑的情況,需要進一步處理,此時可能是“兄左,左子黑或者兄左,左子紅”的情況
                 *
                 * */
                else {
                    /*如果左兄弟節點的左子節點爲null或者爲黑色,那麼屬於“兄左,左子黑”
                     * 處理方法是:以兄弟節點B爲基點左旋,然後BR塗黑,B塗紅,然後將BR當成新B,B當成新BL,這樣就轉換成了情況4-“兄左,左子紅”。
                     * */
                    if (sl == null || !sl.red) {
                        //左兄弟節點的右子節點sr(BR)塗黑,
                        if (sr != null)
                            sr.red = false;
                        //xpl(B)塗紅
                        xpl.red = true;
                        //以兄弟節點xpl(B)爲基點左旋
                        root = rotateLeft(root, xpl);
                        //然後將BR當成新xpl(B),B當成新BL,這樣就轉換成了情況4-“兄左,左子紅”。
                        //實際上旋轉之後BR成爲了xp的left左子節點
                        xpl = (xp = x.parent) == null ?
                                null : xp.left;
                    }
                    /*下面就是“兄左,左子紅”的情況
                     * 處理方法是:以父節點P爲基點右旋,然後交換P和B的顏色(實際上就是兄弟節點的顏色設置爲父節點的顏色,父節點塗黑,因爲兄弟節點肯定是黑色的)
                     * ,BL塗黑,平衡完畢!
                     * */
                    if (xpl != null) {
                        //兄弟節點的顏色設置爲父節點的顏色
                        xpl.red = (xp == null) ? false : xp.red;
                        //sl(BL)塗黑
                        if ((sl = xpl.left) != null)
                            sl.red = false;
                    }
                    if (xp != null) {
                        //父節點塗黑
                        xp.red = false;
                        //以父節點xp(P)爲基點右旋
                        root = rotateRight(root, xp);
                    }
                    /*設置x等於root,那麼將在下一次循環時直接在情況1中退出*/
                    x = root;
                }
            }
        }
    }
}

7 其他方法

  如果我們把put和remove方法的源碼看的差不多了,那麼下面的一些其他方法對於我們來說就是小菜一碟!

7.1 get方法

public V get(Object key)

  返回指定鍵所對應的值;如果找不到這個鍵,則返回 null。返回 null 值並不一定 表明該映射不包含該鍵的映射關係;也可能該映射將該鍵顯示地映射爲 null。可使用 containsKey 操作來區分這兩種情況。

/**
 * 返回指定鍵所對應的值;如果找不到這個鍵,則返回 null。
 * 返回 null 值並不一定 表明該映射不包含該鍵的映射關係;也可能該映射將該鍵顯示地映射爲 null。
 * 可使用 containsKey 操作來區分這兩種情況。
 *
 * @param key 查找的key
 * @return 返回指定鍵所對應的值;如果找不到這個鍵,則返回 null。
 * 返回 null 值並不一定 表明該映射不包含該鍵的映射關係;也可能該映射將該鍵顯示地映射爲 null。
 */
public V get(Object key) {
    Node<K, V> e;
    //內部調用getNode方法
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
 * 根據key獲取具有相同key的Node節點
 *
 * @param hash key的hash
 * @param key  key
 * @return 相同key的Node節點或者null
 */
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) {
        /*判斷key相等的條件是:兩個key的hash相同,並且兩個key的equals或者==比較返回true。*/
        /*1 如果數組桶位的節點的key是相同的,那麼返回該節點,夠則需要查詢鏈表或者紅黑樹*/
        if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        /*2 查詢鏈表或者紅黑樹*/
        if ((e = first.next) != null) {
            /*如果屬於紅黑樹節點類型,那麼通過getTreeNode獲取與指定key相同的節點
             * getTreeNode方法在“removeNode移除節點的總方法”部分已經將過了
             * */
            if (first instanceof TreeNode)
                return ((TreeNode<K, V>) first).getTreeNode(hash, key);
            /*否則,遍歷普通鏈表獲取與指定key相同的節點*/
            do {
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    /*到這一步還沒返回說明沒有查到找與指定key相同的節點,返回null*/
    return null;
}

7.2 containsKey方法

public boolean containsKey(Object key)

  如果此map包含指定key,則返回 true。

/**
 * 如果此map包含指定key,則返回 true。
 *
 * @param key k
 * @return 如果此map包含指定key,則返回 true。
 */
public boolean containsKey(Object key) {
    //內部就是調用的getNode方法,並判斷返回值是不是null來返回結果
    //如果getNode返回null,那麼返回false,否則返回true
    return getNode(hash(key), key) != null;
}

7.3 containsValue方法

public boolean containsValue(Object value)

  如果此map包含指定值,則返回 true。該方法性能比較低,因爲對於數組、鏈表還是紅黑樹都採用的是順序遍歷,即需要遍歷整個哈希表。

/**
 * 如果此map包含指定值,則返回 true。
 * 該方法性能比較低,因爲對於數組、鏈表還是紅黑樹都採用的是順序遍歷,並且需要遍歷整個哈希表。
 *
 * @param value 指定值
 * @return 如果此map包含指定值,則返回 true。
 */
public boolean containsValue(Object value) {
    Node<K, V>[] tab;
    V v;
    if ((tab = table) != null && size > 0) {
        /*循環底層數組*/
        for (int i = 0; i < tab.length; ++i) {
            /*循環每一個數組的鏈表,可能是普通鏈表,也可能是紅黑樹鏈表*/
            for (Node<K, V> e = tab[i]; e != null; e = e.next) {
                /*判斷相等的條件是:==返回true 或者 equals方法返回true*/
                if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    //遍歷全部哈希表還沒找到那麼返回false
    return false;
}

7.4 putAll方法

public void putAll(Map<? extends K,? extends V> m)

  將指定map的所有數據複製到此map中,只是複製了數據的引用,即兩個map的不同節點指向同一個key或者value對象。內部實際上就是循環調用putVal方法!

/**
 * 將指定map的所有數據複製到此map中,只是複製了數據的引用,即兩個map的不同節點指向同一個key或者value對象。
 * @param m 指定map
 */
public void putAll(Map<? extends K, ? extends V> m) {
    //內部調用putMapEntries方法
    putMapEntries(m, true);
}

/**
 * 將指定map添加到該map集合
 *
 * @param m     指定map
 * @param evict 在構造器中調用該方法時傳入false,其他方法中(比如put、putAll)調用該方法時傳入true。
 *              實際上該參數在HashMap中沒啥用,是留給其子類linkedHashMap用於實現LRU緩存的!
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //獲取節點數量
    int s = m.size();
    //如果大於0
    if (s > 0) {
        /*如果本集合table爲null,那麼初始化*/
        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);
        }
        /*否則,如果s大於擴容閾值,那麼直接擴容*/
        else if (s > threshold)
            resize();
        /*循環指定map,實際上就是調用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);
        }
    }
}

7.5 clear方法

public void clear()

  清空哈希表。並沒有新建數組,而是循環底層數組,將每一個桶位置空。

/**
 * 清空哈希表
 */
public void clear() {
    Node<K, V>[] tab;
    modCount++;
    /*並沒有新建數組,而是循環底層數組,將每一個桶位置空*/
    if ((tab = table) != null && size > 0) {
        //將size置空
        size = 0;
        for (int i = 0; i < tab.length; ++i)
            tab[i] = null;
    }
}

7.6 遍歷的方法

  遍歷的方法主要有三個values()、keySet()、entrySet(),都是實現自超級接口Map。因此實際上HashMap和HashTable的遍歷方法的底層實現都差不多,實際上獲取的一系列鍵集、值集、鍵值對等,實際上都是操作的底層數組,因此可以對這個返回的結合進行操作來操作底層數組的數據。
  遍歷方法的源碼詳解,在:Java集合—Hashtable的源碼深度解析以及應用介紹一文中有詳細介紹。

Set< Map.Entry< K,V > > entrySet()

  將該映射所有的鍵值對(鍵值對類型是Map.entry類型),返回並存放在一個Set 集合當中,獲得set集合後可以遍歷得到每個項的鍵值對,然後可以使用Map.entry中的提供的getkey、getValue、setValue等方法

Set< K > keySet()

  返回包含該映射所有key的set集合,得到set集合後可遍歷得到每個key,再通過:map.get(key)即可得到對應的value值。

Collection< V > values()

  返回包含該映射所有的value值的 Collection 集合。 通過遍歷該集合,可以得到映射的value。但不能得到key。

8 Hashmap 在JDK1.7和1.8的某些區別

8.1 數據結構

  JDK1.7的時候使用的是數組+ 單鏈表的數據結構。JDK1.8及之後時,使用的是數組+鏈表+紅黑樹的數據結構。使用紅黑樹主要時當鏈表過長時,對某個桶位元素的查找變成了線性時間O(n),轉換爲紅黑樹之後查詢時間複雜度從變成O(logN)提高了查找效率。但是也提升了實現難度。
  JDK1.8及之後,在使用put方法添加元素時,當添加元素之後鏈表的長度 大於8 、數組長度 大於等於64 時(小於64會擴容一次),就會把普通鏈表轉成紅黑樹的數據結構。
  在擴容時,如果原紅黑樹在split方法中拆分出的紅黑樹鏈表長度 小於等於6 時,紅黑樹還原爲普通鏈表;否則,紅黑樹鏈表將會轉換爲紅黑樹。
  在刪除元素時,刪除元素之前如果紅黑樹結構滿足如下要求之一:root == null || root.right == null || (rl = root.left) == null || rl.left == null,那麼紅黑樹將還原爲普通鏈表!即節點數量在[3,10]之間,紅黑樹都將可能還原爲普通鏈表!

8.2 鏈表插入數據方式

  JDK1.7用的是頭插法,而JDK1.8及之後使用的都是尾插法。採用頭插法時會容易出現逆序且環形鏈表死循環問題。但是在JDK1.8之後是因爲加入了紅黑樹使用尾插法,能夠避免出現逆序且鏈表死循環的問題。
  具體原因在前面添加元素部分有詳解!

8.3 擴容機制

  添加元素時,JDK1.7是先判斷是否需要擴容,然後再插入新數據,JDK1.8是先插入數據,然後判斷是否需要擴容。
  JDK1.7的擴容要求是:(size >= threshold) && (null != table[bucketIndex])都滿足纔會擴容,即要求添加節點前(節點數量大於等於閾值並且這個新節點的桶位已經有了節點(即發生哈希衝突)纔會擴容;
  JDK1.8JDK1.7的擴容要求是++size> threshold即添加節點後++size如果大於擴容閾值就會擴容。
  JDK1.7在轉移數據時,節點的位置計算採用HashCosde()–>擾動算法–>h&(length-1)的方式重新計算,效率較低,並且轉移時也採用頭插法轉移數據。
  JDK1.8在轉移數據時,節點的位置通過規律(e.hash & oldCap) == 0來判斷新位置處於原始位置還是原始位置+老容量的位置,效率較高,並且轉移時也相當於採用尾插法轉移數據。

8.4 擾動算法

  擾動算法用於進一步降低哈希衝突的概率。JDK1.7採用的擾動算法是四次>>>運算加五次^運算,JDK1.8採用的擾動算法是一次>>>運算加一次 ^ 運算,效率更高!

9 總結

  下面我儘量給出一幅考慮多種情況的JDK1.8的HashMap結構圖,如果你真的看完了我上面所寫的全部內容,那麼你應該不會對某些結構感到疑惑:
在這裏插入圖片描述
  HashMap是我們使用的最多的集合類之一了。一般用來存放鍵值對,並且性能比較好。key不能重複,判斷兩個key是否相等的依據是:(兩個key的HashCode返回值相等)並且(兩個key使用==比較返回true或者使用equals比較返回true) 。它是線程不安全,在併發環境下想要使用可以使用ConCurrentHashMap,關於JUC的源碼,我將會在後續博客中一一分析!
  寫這篇文章花費了本人長達兩天的時間,特別是在put和remove方法深入到最底層的紅黑樹源碼的分析過程中,甚至有幾次都想到放棄。本人只是一個從傳統工科(採礦)轉行到計算機行業的Java開發者,而且是在畢業工作了一年之後才嘗試轉行的,並沒有本專業的開發者那麼多基礎知識儲備。
  雖然目前轉行之後還算過得去,但是仍然記得在當初學習的時候,就覺得HashMap沒那麼簡單,對於某個知識,我不想只會用,還想要理解它的原理。在工作之後,念念不忘,終於在今天還算比較徹底的分析了HashMap的主要方法的源碼,但是裏面說不定有很多錯誤,歡迎大家指出來,希望大家一起進步!

如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!

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