最近面試被問HashMap
容器的實現原理,答的一塌糊塗。。。雖說一直唸叨着說要看看Java
容器的源碼,但總是被耽擱了,今天終於靜下心來看了🤦♂️。
註明:以下源碼分析都是基於jdk 1.8.0_221
版本
HashMap源碼分析目錄
一、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;
上面提到的容量
就是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源碼分析,各位小夥伴可以對比着看,效果更佳哦~