Java容器之HashMap源碼分析(媽媽再也不用擔心我不懂HashMap了)

  最近面試被問HashMap容器的實現原理,答的一塌糊塗。。。雖說一直唸叨着說要看看Java容器的源碼,但總是被耽擱了,今天終於靜下心來看了🤦‍♂️。

  註明:以下源碼分析都是基於jdk 1.8.0_221版本
在這裏插入圖片描述

一、HashMap概述(一圖以蔽之

  HashMap的類聲明如下

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

  HashMap是一個<key,value>(或稱鍵值對)容器,其底層實現是使用一個hash數組指向多個不同的鏈表。每次我們放入一個<key,value>,它會自動計算key對應的hash值,然後根據hash值插入到不同的鏈表中。
在這裏插入圖片描述

  註明:可能會有人對爲啥要用hash數組鏈表產生疑問,這是因爲實際插入過程中會出現多個<key,value>的key計算出的hash值相同(哈希衝突),如上圖的table[1]。但是當鏈表太長時,在容器中查找<key,value>,每次都要遍歷耗時長,降低了查找效率,所以在Java 8中,引入了紅黑樹。默認當某個hash值下超過了8個<key,value>,此時就需要轉化成紅黑樹,如果上圖中的table[14]

二、HashMap類的屬性

1、HashMap類靜態屬性

/**
 * 序列化的版本號
 */
private static final long serialVersionUID = 362498820763181265L;
/**
 * 默認的初始化容量大小,並且必須是2的冪(主要是考慮效率,後面有介紹)
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * 最大的容量(容器中存放<key, value>的最大數量)
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 負載因子
 * 當容器中<key, value>的數量超過capacity * DEFAULT_LOAD_FACTOR時,需要擴容
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 鏈表轉紅黑樹閾值
 * 當某個hash值下<key, value>用鏈表存儲,並且鏈表長度不小於該值,就需要轉成紅黑樹
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 紅黑樹轉鏈表閾值
 * 當某個hash值下<key, value>是用紅黑樹存儲,並且樹中的節點數小於該值,就需要轉成鏈表
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 最小樹形化容量閾值
 * 當哈希表中的容量 > 該值時,才允許將鏈表轉成紅黑樹操作,否則直接擴容。
 * 爲了避免進行擴容、鏈表轉紅黑樹選擇的衝突,並且這個值不能小於 4 * TREEIFY_THRESHOLD(鏈表轉紅黑樹閾值)
 */
static final int MIN_TREEIFY_CAPACITY = 64;

2、HashMap非靜態屬性

  transient關鍵字的作用是在序列化的時候排除該屬性,比如寫入硬盤持久化,用這個關鍵字修飾的屬性在對象保存時不會寫入。(不過HashMap類在尾端重寫了序列化方法,手動指定了需要序列化的屬性)

/**
 * table數組,也稱hash桶數組
 */
transient Node<K,V>[] table;

/**
 * entrySet屬性,把<K,V>存放到Set容器中(一般hashmap的遍歷用此屬性)
 */
transient Set<Map.Entry<K,V>> entrySet;

/**
 * 容器key-value數量(注意與容器的容量(容器可存放的數量)不同)
 */
transient int size;

/**
 * 容器進行結構性調整(增加或者刪除鍵值對等操作,不包括修改value值)的次數
 */
transient int modCount;

/**
 * 容器中能容納的key-value極限,capacity * loadFactor,超過就需要擴容
 */
int threshold;

/**
 * 負載因子,默認是0.75(前面類的靜態屬性已經定義過了)
 */
final float loadFactor;

\color{red}注意:上面提到的容量就是table數組的長度,size是容器中存放的key-value數量,threshold = 容量 * 負載因子,表示的該容器最多可以放置多少個key-value

三、HashMap類的構造器

  查看HashMap類文件,可以發現一共有4個構造器。

/**
 * @param  initialCapacity 初始化容量大小
 * @param  loadFactor      負載因子
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
public HashMap(int initialCapacity, float loadFactor) {
	// 檢查initialCapacity的合法性
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    // 檢查initialCapacity是否超過了可設置的最大容量(類靜態屬性)
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 檢查loadFactor負載因子的合法性
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    //初始化threshold稍微複雜一點,tableSizeFor方法解析見本博客尾端
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * @param  initialCapacity 初始化容量大小
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
public HashMap(int initialCapacity) {
	//默認負載因子爲0.75
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 只設置負載因子爲0.75,其它值全部默認
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}

/**
 * 複製構造函數,將另外一個map初始化構造
 *
 * @param   m 其它map容器
 * @throws  NullPointerException
 */
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //將m容器中的所有entry放入新建的容器對象中
    putMapEntries(m, false);
}

四、增加key-value相關方法

1、put方法

  put方法,往容器中添加key-value,允許key = null,也允許value = null

/**
 * 往容器中添加`key-value`,允許`key = null`,也允許`value = null`
 */
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

2、putVal方法

  putVal方法的作用是往map容器中插入一個key-value

/**
 * Implements Map.put and related methods.
 *
 * @param hash key的hash值(調用hash()方法)
 * @param key 插入鍵值對key
 * @param value 插入鍵值對value
 * @param onlyIfAbsent 設爲true時,表示如果容器已經存在這個key就不進行修改
 * @param evict 爲 false時,表示容器正處於創建(其它map傳入初始化)
 * @return previous value, or null if none
 * 
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab指向 對象的table數組(hash桶數組),p 指向hash對應的桶
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果容器爲空,則需要調用resize方法,初始化table數組
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 將p指向hash對應的hash桶
    if ((p = tab[i = (n - 1) & hash]) == null)
    	// (n - 1) & hash求出hash對應的table數組下標,如果這個位置爲空,說明這個桶爲空
    	// 直接放入table中,不需要生成鏈表、紅黑樹等
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        // 根據p(指向對應的hash桶),找到key對應的節點位置
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
        	// 如果當前p指向桶第一個元素就是key
            e = p;
        else if (p instanceof TreeNode)
        	// 如果p指向的內容(hash桶對應的第一個元素)是紅黑樹的對象,說明該桶已轉換爲紅黑樹,調用putTreeVal插入
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
        	// 否則桶內實現是鏈表,只能遍歷鏈表查找key
            for (int binCount = 0; ; ++binCount) {
            	// p.next == null。即鏈表的尾端
                if ((e = p.next) == null) {
                	// 還沒找到,則需要插入節點
                    p.next = newNode(hash, key, value, null);
                    // 如果該桶的元素超過了 TREEIFY_THRESHOLD,需要進行擴容或者轉成紅黑樹
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //如果當前節點以及成功匹配key,退出
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) {
        	//找到了key對應的位置,再賦value
            V oldValue = e.value;
            // onlyIfAbsent入口參數,爲true,則不更新value(前面已說明)
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //成功更新了節點
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //容器中的key-value數自增,並且判斷是否需要擴容(前面已多次說明threshold = 容器容量 * 最大負載因子)
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

3、putMapEntries方法

  putMapEntries方法作用是將其他map容器中的key-value複製到本容器中。

/**
 * 將一個map容器中的key-value複製到本容器
 *
 * @param m 其它map容器
 * @param evict 爲 false時,表示容器正處於創建(其它map傳入初始化)
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    //只有當m非空的時候纔有遍歷的必要
    if (s > 0) {
    	//如果table爲空,則需要判斷m中的元素個數是否會超過本容器可容納的數量(容器容量 * 負載因子)
        if (table == null) { // pre-size
        	//由於table爲空,我們需要將 m.size() / 負載因子loadFactor,得到需要的最小空間
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
            //最小空間都大於容器當前能存放的最大數量(threshold = 當前容量 * 負載因子)
            if (t > threshold)
            	//當前容器無法容納,則需要計算不必t小的2的冪
                threshold = tableSizeFor(t);
        }
        else if (s > threshold)
        	// table != null,此時我們只要判斷 m.size() > 容器當前能存放的最大數量(threshold = 當前容量 * 負載因子),從而決定是否擴容
            resize();
        // 經過上面的擴容操作,已經保證 m.size() < 容器當前能存放的最大數量(threshold = 當前容量 * 負載因子),遍歷m放入本容器即可
        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);
        }
    }
}

五、刪除key-value相關方法

1、remove方法

  remove方法是提供給外界的刪除key-value的接口。

/**
 * 根據key刪除key-value,刪除成功返回對應的value,否則返回null
 */
public V remove(Object key) {
    Node<K,V> e;
    //調用removeNode方法
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

2、removeNode方法

  removeNode方法是刪除key-value的具體實現(注意不對外展示)。

/**
 * Implements Map.remove and related methods.
 *
 * @param hash key對應的hash(調用hash()方法即可計算得到)
 * @param key 待刪除的key-value對應的key
 * @param value key-value對應的value,只有當matchValue == true時,此參數纔有意義
 * @param matchValue 如果設爲true,刪除的時候還需要匹配value才能刪
 * @param movable 設爲false,表示刪除成功了不移動其它節點(一般設爲true,即刪除節點後需要進行調整)
 * @return 刪除成功則返回key對應的value,否則返null
 */
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
	// tab用於指向table數組(hash桶數組),p用於指向傳入的key對應的hash所對應的hash桶
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 如果table數組存在hash對應的桶
    if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
    	// node的作用是指向key對應的節點位置
        Node<K,V> node = null, e; K k; V v;
        // 判斷hash桶的第一個元素的key是否是匹配成功
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
        	// 匹配不成功,則需要判斷這個桶是鏈表還是紅黑樹實現
            if (p instanceof TreeNode)
            	// 如果是 紅黑樹則調用getTreeNode方法
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
            	// 否則只能遍歷鏈表
                do {
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        // 如果node != null,則說明容器中存在key對應的key-value
        // 如果 matchValue == true,則還需要匹配value,才能刪
        if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
            // 如果node指向的對象是TreeNode類型,則調用紅黑樹對應的remove方法即可
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
            	// 如果node是table數組中的元素(hash桶內第一個元素)
                tab[index] = node.next;
            else
            	// 否則node是鏈表中的其他節點
                p.next = node.next;
            // 刪除節點是結構性調整
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

六、查找key-value相關方法

1、get方法

  get方法的作用是根據key查找value

/**
 * 根據`key`查找`value`。
 * @see #put(Object, Object)
 */
public V get(Object key) {
    Node<K,V> e;
    //調用getNode方法查找
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

2、getNode方法

  getNode方法的作用是根據key查找查找<key, value>

/**
 * 根據`key`查找查找`<key, value>`
 *
 * @param hash key對應的hash值(調用hash()方法即可獲取)
 * @param key the key
 * @return the node, or null if none
 */
final Node<K,V> getNode(int hash, Object key) {
	// tab用於指向table數組(hash桶數組),first用於指向hash桶第一個元素,e用於指向hash、key匹配到的節點位置
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //查找table數組(hash桶數組)中是否存在hash對應的桶
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        //判斷桶中的第一個元素first的key與需要查找的key是否想相同
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //這個桶不能只有一個元素, > 1纔有繼續尋找的必要
        if ((e = first.next) != null) {
            if (first instanceof TreeNode)
            	//如果該hash桶是紅黑樹實現,調用紅黑樹對應的查找方法
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            //否則該hash桶是鏈表實現,遍歷鏈表即可
            do {
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

3、containsKey方法

  containsKey方法的作用是判斷容器中是否存在key對應的key-value

/**
 * 判斷容器中是否存在`key`對應的`key-value`
 */
public boolean containsKey(Object key) {
	// 調用getNode方法,如果查找到了key-value則說明存在
    return getNode(hash(key), key) != null;
}

4、containsValue方法

  containsValue方法的作用是判斷容器中是否存在value對應的key-value

/**
 * 判斷容器中是否存在`value`對應的`key-value`
 */
public boolean containsValue(Object value) {
	// tab用於指向table數組(hash桶數組)
    Node<K,V>[] tab; V v;
    if ((tab = table) != null && size > 0) {
    	//遍歷table數組(hash桶數組)
        for (int i = 0; i < tab.length; ++i) {
        	// 遍歷hash桶中的每一個元素
            for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                if ((v = e.value) == value ||
                    (value != null && value.equals(v)))
                    return true;
            }
        }
    }
    return false;
}

七、其它方法

1、tableSizeFor方法

  該方法用於找出不小於cap的最小2的冪。
  假設cap 的二進制形式爲01xxxx...xxx,先考慮n = cap的情況.

n n >>> x n | = n >>> x
初始化爲cap = 01xxxx...xxx n >>> 1結果 001xxx...xxx 011xxx...xxx
011xxx...xxx n >>> 2結果 00011x...xxx 01111x...xxx
01111x...xxx n >>> 4結果 000001111xxx 011111111...

  表格所表達的意思就是依次將cap最高位爲1後面的所有位都置爲1,第一次右移一位,n |= n >>> 1得到了兩個1,第二次n |= n >>> 2,右移兩位,得到了4個1,然後右移4位,得到了8個1…
  然後n += 1,也就是二進制011...111進位100...000,正好是2的冪。

/**
 * 返回不小於cap的最小的2的冪,比如cap == 3時,返回4,cap == 12時返回16等等
 */
static final int tableSizeFor(int cap) {
	// n = cap - 1是爲了防止當cap本身就是2的冪,此時計算出的結果偏大了
	// 比如cap = 16(二進制”10000“),通過計算求得的n = "11111”,
	// 然後n + 1 = "100000" = 32, 實際上cap自身16就是解
    int n = cap - 1;
    // >>> 運算符的作用是 無符號右移(左邊填充0),比如 ’11111‘ >>> 1的結果爲'01111'
    // 如果你知道原碼、補碼的相關知識,這點很容易理解,不知道就先記着吧
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    //最後返回n + 1、MAXIMUM_CAPACITY(HashMap容器容量最大值)的較小值,
    //由於n計算時可能發生了溢出,所以需要判斷是否小於0
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

  如果你還是沒看懂這個解釋,可以隨便帶入幾個cap,比較計算後的二進制結果就會發現這個方法的作用。

2、hash方法(重要)

  此方法用來計算key對象的hash值,從而決定放到table表hash桶)的哪個表項中。

/**
 * 計算key對象的hash值
 */
static final int hash(Object key) {
    int h;
    // hashCode是Object類的方法(一般重寫tostring方法會讓你重寫這個方法)
    // 在HashMap容器中,hash值 == key.hashCode()的前16位 異或 key.hashCode()的後16位(主要是防止hash衝突,多個key的hash值一樣)
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

3、resize方法(重要)

  resize方法的作用是擴容。

/**
 * Initializes or doubles table size.  If null, allocates in
 * accord with initial capacity target held in field threshold.
 * Otherwise, because we are using power-of-two expansion, the
 * elements from each bin must either stay at same index, or move
 * with a power of two offset in the new table.
 *
 * @return the table
 */
final Node<K,V>[] resize() {
	// 記錄擴容前的狀態
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果oldCap > 0,說明不是初始化
    if (oldCap > 0) {
    	// 如果oldCap 不小於HashMap容器定義的最大容量,修改threshold爲最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // oldCap * 2後是否超過 MAXIMUM_CAPACITY
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 容量變爲原來的2倍,可存放的閾值(最大容量 * 負載因子)也 * 2,
            // 由於容量 * 2,所以閾值也需要 * 2
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0)
    	// 根據前面判斷知oldCap <= 0,此時時調用了HashMap的帶參構造器,初始容量用threshold替換,
        //在帶參構造器中,threshold的值爲 tableSizeFor() 的返回值,也就是2的冪,而不是 capacity * load factor
        newCap = oldThr;
    else {
    	// 根據前面兩個判斷,oldCap <= 0 且 oldThr > 0,即調用了默認構造器
    	// 此時容器容量 newCap 賦值默認初始化容量,
    	// 容器最大存放數量newThr 賦值 默認負載因子 * 默認初始化容量
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
    	//重新計算閾值
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    // 更新當前容器可存放的最大數量
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    // 創建新的 table數組(hash桶數組)
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
    	//將oldTab中的所有key-value複製到newTab,遍歷oldTab數組(hash桶數組)
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //當前hash桶不爲空纔有遍歷的必要
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                // 如果該hash桶中國只有一個元素,直接複製
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果該hash桶是紅黑樹實現,調用split方法複製
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else {
                	// 否則該hash桶是鏈表實現,需要遍歷鏈表
                	// 將源鏈表拆分根據成兩個鏈表,原鏈表中的所有節點(Node.hash % oldCap) == 0
                	// loHead、loTail指向第一個鏈表的頭、尾,鏈表中的(Node.hash & oldCap) == 0
                	// hiHead、hiTail指向第二個鏈表的頭、尾,鏈表中的(Node.hash & oldCap) != 0
                    
                    // 比如 oldCap = 16時,hash = 13,29,45,61...都應該放在oldTab[12]這個桶下
                    // 先應 newCap = 2 * oldCap = 32,需要拆分成newTab[12]、newTab[12 + oldCap = 28]兩個桶
                    // newTab[12] = [13, 45],  hash % newCap = hash % 32 = 13
                    // newTab[12 + oldCap = 28] = [29, 61], hash % newCap = hash % 32 = 28
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                        	//放到第一個鏈表
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                        	//放到第二個鏈表
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //然後分別將第一個鏈表放入newTab[j]
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //第二個鏈表放入newTab[j + oldCap]
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

4、treeifyBin方法(重要)

  treeifyBin方法是將某個鏈表實現的hash桶轉換爲紅黑樹。

/**
 * @parm hash 代轉換成紅黑樹的hash桶對應的hash值
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    // 如果容器的(長度)容量小於 MIN_TREEIFY_CAPACITY,則直接擴容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    // 否則 hash對應的 桶不爲空時,此時進行鏈表轉紅黑樹操作
    else if ((e = tab[index = (n - 1) & hash]) != null) {
    	// (n - 1) & hash的作用是獲取hash桶對應的下標(table數組),效果等同於 hash % n(n 是 tab數組的長度),
    	// 這是由於n 是 2 的次冪,這也是爲什麼table的容量(長度)必須初始化爲2 的次冪,簡化求餘操作
        TreeNode<K,V> hd = null, tl = null;
        // 遍歷 tab[(n - 1) & hash],將所有節點都轉成 TreeNode 節點
        do {
        	// 將當前節點轉換爲 TreeNode 節點(注意此處並沒轉成紅黑樹)
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        // 然後在調用treeify方法,將hd鏈表轉成紅黑樹
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

八、總結(一圖以蔽之)

在這裏插入圖片描述
  HashMap由一個table數組hash桶數組)和若干個鏈表組成,當某個hash桶內的數量過多時(鏈表太長,查找效率低),此時需要將鏈表轉結構成紅黑樹結構,默認是鏈表長度超過8就要轉換(當然如果紅黑樹中的節點太少,默認是 < 6時,需要轉換回鏈表結構)。每次插入、刪除節點,只要維持table數組、各個鏈表、紅黑樹即可。

已更新 Java容器之Hashtable源碼分析,各位小夥伴可以對比着看,效果更佳哦~

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