對(duì)HashMap的數(shù)據(jù)結(jié)構(gòu)的學(xué)習(xí)解讀
1、介紹
JDK1.8之前HashMap由數(shù)組+鏈表組成。數(shù)組是HshMap的主體,鏈表則是主要為了解決哈希沖突。
JDK1.8及以后的版本在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長度大于閾值(或者紅黑樹的邊界值,默認(rèn)為8,且數(shù)組中桶的數(shù)量大于64,此時(shí)此索引位置上的所有數(shù)據(jù)改為使用紅黑樹存儲(chǔ)。
1.1、數(shù)據(jù)結(jié)構(gòu)
如下圖所示:

桶數(shù)組是用來存儲(chǔ)數(shù)據(jù)元素,鏈表是用來解決沖突,紅黑樹是為了提高查詢的效率。
數(shù)據(jù)元素通過映射關(guān)系,也就是散列函數(shù),映射到桶數(shù)組對(duì)應(yīng)索引的位置。
1.如果發(fā)生沖突,從沖突的位置拉一個(gè)鏈表,插入沖突的元素。
- 如果鏈表長度>8&數(shù)組大小>=64,鏈表轉(zhuǎn)為紅黑樹。
- 如果紅黑樹節(jié)點(diǎn)個(gè)數(shù)<6 ,轉(zhuǎn)為鏈表。
2.但是數(shù)組長度小于64,此時(shí)并不會(huì)將鏈表變?yōu)榧t黑樹。而是選擇進(jìn)行數(shù)組擴(kuò)容。
- 目的是因?yàn)閿?shù)組比較小,盡量避開紅黑樹結(jié)構(gòu),這種情況下變?yōu)榧t黑樹結(jié)構(gòu),反而會(huì)降低效率,因?yàn)榧t黑樹需要進(jìn)行左旋,右旋,變色這些操作來保持平衡 。同時(shí)數(shù)組長度小于64時(shí),搜索時(shí)間相對(duì)要快些。
所以綜上所述為了提高性能和減少搜索時(shí)間,底層在閾值大于8并且數(shù)組長度大于64時(shí),鏈表才轉(zhuǎn)換為紅黑樹。
至于為什么要轉(zhuǎn)變從鏈表轉(zhuǎn)到紅黑樹,以及紅黑樹轉(zhuǎn)鏈表:JDK8 HashMap紅黑樹退化為鏈表的機(jī)制解讀
1.2、紅黑樹
為什么要使用紅黑樹:
如果鏈表非常長(特別是在某些不均勻分布的哈希情況下),查找、插入和刪除的性能會(huì)降到 O(n) 的水平,因?yàn)樾枰闅v整個(gè)鏈表。
在這里簡單介紹下紅黑樹:

由上圖可知:
紅黑樹本質(zhì)上是一種二叉查找樹,為了保持平衡,它又在二叉查找樹的基礎(chǔ)上增加了一些規(guī)則:
1、每個(gè)節(jié)點(diǎn)要么是紅色,要么是黑色;
2、根節(jié)點(diǎn)永遠(yuǎn)是黑色的;
3、所有的葉子節(jié)點(diǎn)都是是黑色的(注意這里說葉子節(jié)點(diǎn)其實(shí)是圖中的 NULL 節(jié)點(diǎn));
4、每個(gè)紅色節(jié)點(diǎn)的兩個(gè)子節(jié)點(diǎn)一定都是黑色;
從任一節(jié)點(diǎn)到其子樹中每個(gè)葉子節(jié)點(diǎn)的路徑都包含相同數(shù)量的黑色節(jié)點(diǎn);
這種方式可以在最壞情況下將查找、插入和刪除的時(shí)間復(fù)雜度降低到 O(log n)。
紅黑樹怎么保持平衡?
紅黑樹有兩種方式保持平衡:旋轉(zhuǎn)和染色。
旋轉(zhuǎn)分為左旋和右旋。
1、左旋

2、右旋

1.3、參數(shù)
查看HashMap源碼,可以看到以下常用的變量,因此先在這里進(jìn)行歸類:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4:默認(rèn)的table初始容量 static final float DEFAULT_LOAD_FACTOR = 0.75f:默認(rèn)的負(fù)載因子 static final int TREEIFY_THRESHOLD = 8: 鏈表長度大于等于該參數(shù)轉(zhuǎn)紅黑樹 static final int UNTREEIFY_THRESHOLD = 6: 當(dāng)樹的節(jié)點(diǎn)數(shù)小于等于該參數(shù)轉(zhuǎn)成鏈表 transient Node<K,V>[] table:這是一個(gè)Node類型的數(shù)組(也有稱作Hash桶)
可以從下面源碼中看到靜態(tài)內(nèi)部類Node在這邊可以看做就是一個(gè)節(jié)點(diǎn),多個(gè)Node節(jié)點(diǎn)構(gòu)成鏈表,當(dāng)鏈表長度大于8的時(shí)候轉(zhuǎn)換為紅黑樹。
2、成員變量
Node 類
HashMap 內(nèi)部使用的一個(gè)重要結(jié)構(gòu)是 Node,通常定義為一個(gè)靜態(tài)內(nèi)部類,用于存儲(chǔ)鍵值對(duì)。
static class Node<K, V> {
final int hash; // 鍵的哈希值
final K key; // 鍵
V value; // 值
Node<K, V> next; // 指向下一個(gè)節(jié)點(diǎn),處理鏈表沖突
}1. Node<K,V>[] table
- 類型:
Node<K,V>[] - 描述: 這是一個(gè)數(shù)組,存儲(chǔ)了 HashMap 的所有 "桶"(bucket)。
- 作用: 每個(gè)桶對(duì)應(yīng)一個(gè)鏈表(或紅黑樹)以解決哈希沖突。桶的 index 通過取鍵的哈希值并與數(shù)組的長度進(jìn)行模運(yùn)算得到。
2. int size
- 類型:
int - 描述: 當(dāng)前 HashMap 中鍵值對(duì)的數(shù)量。
- 作用: 用于追蹤 HashMap 中的元素?cái)?shù)量,便于在執(zhí)行操作時(shí)(如添加或刪除元素)進(jìn)行容量檢查。
3. int threshold
- 類型:
int - 描述: 擴(kuò)容閾值,即當(dāng) HashMap 中的元素?cái)?shù)量達(dá)到該值時(shí),將觸發(fā)擴(kuò)容。
- 作用: 閾值由
capacity * load factor計(jì)算得出。它幫助管理 HashMap 的容量和負(fù)載因子,防止在高負(fù)載下性能下降。
4. float loadFactor
- 類型:
float - 描述: 負(fù)載因子是一個(gè)決定 HashMap 擴(kuò)容策略的參數(shù)。默認(rèn)值是 0.75。
- 作用: 它用于衡量 HashMap 中使用的空間與實(shí)際容納的空間之間的比率。當(dāng) HashMap 中的元素?cái)?shù)量超過這個(gè)比率所計(jì)算的閾值時(shí),它將擴(kuò)容。
通過合理的負(fù)載因子設(shè)置:
不同的負(fù)載因子會(huì)影響 HashMap 的性能。較低的負(fù)載因子(例如 0.5)會(huì)使得 HashMap 更頻繁地?cái)U(kuò)容,從而減少?zèng)_突并提高快速訪問的性能,但會(huì)浪費(fèi)內(nèi)存。
高負(fù)載因子的影響:
較高的負(fù)載因子(例如 0.9)可以提高內(nèi)存使用效率,但同時(shí)也增加了哈希沖突的風(fēng)險(xiǎn),這可能導(dǎo)致訪問性能下降(例如,查詢、插入和刪除的時(shí)間復(fù)雜度從 O(1) 變?yōu)?O(n))。
以下代碼示例比較了使用不同負(fù)載因子的 HashMap 在大量插入元素時(shí)的性能差異:
import java.util.HashMap;
public class HashMapPerformanceTest {
public static void main(String[] args) {
int numberOfElements = 1_000_000; // 要插入的元素?cái)?shù)量
// 測試負(fù)載因子為默認(rèn)值 (0.75)
long timeWithDefaultLoadFactor = testHashMapPerformance(0.75f, numberOfElements);
System.out.println("Time taken with default load factor (0.75): " + timeWithDefaultLoadFactor + " ms");
// 測試負(fù)載因子為 0.5
long timeWithLowLoadFactor = testHashMapPerformance(0.5f, numberOfElements);
System.out.println("Time taken with low load factor (0.5): " + timeWithLowLoadFactor + " ms");
// 測試負(fù)載因子為 0.9
long timeWithHighLoadFactor = testHashMapPerformance(0.9f, numberOfElements);
System.out.println("Time taken with high load factor (0.9): " + timeWithHighLoadFactor + " ms");
}
private static long testHashMapPerformance(float loadFactor, int numberOfElements) {
long startTime = System.currentTimeMillis();
HashMap<Integer, String> map = new HashMap<>(16, loadFactor); // 初始化 HashMap
// 插入元素
for (int i = 0; i < numberOfElements; i++) {
map.put(i, "Value " + i);
}
long endTime = System.currentTimeMillis();
return endTime - startTime; // 返回耗時(shí)
}
}輸出結(jié)果:
Time taken with default load factor (0.75): 197 ms
Time taken with low load factor (0.5): 716 ms
Time taken with high load factor (0.9): 552 ms
5. int modCount
- 類型:
int - 描述: 記錄 HashMap 進(jìn)行結(jié)構(gòu)性修改的次數(shù)。
- 作用: 用于實(shí)現(xiàn) fail-fast 機(jī)制,能夠檢測到在迭代 HashMap 時(shí)是否發(fā)生了并發(fā)修改,通過與迭代器中的記錄值進(jìn)行比較來判斷。
關(guān)于fail-fast機(jī)制:可參考JDK8 HashMap紅黑樹退化為鏈表的機(jī)制解析
3、構(gòu)造方法
以下有四種不同的初始化方式。
1、無參構(gòu)造
//構(gòu)造一個(gè)空的HashMap,默認(rèn)初始容量16和負(fù)載因子0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
2、有初始化容量構(gòu)造
//構(gòu)造一個(gè)指定初始容量的HashMap,默認(rèn)負(fù)載因子0.75。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}3、指定容量和負(fù)載因子
//構(gòu)造一個(gè)指定的HashMap,指定默認(rèn)初始容量和負(fù)載因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}4、映射Map
//構(gòu)造一個(gè)映射的map
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}HashMap和HashSet都允許你指定負(fù)載因子的構(gòu)造器,表示當(dāng)負(fù)載情況達(dá)到負(fù)載因子水平的時(shí)候,容器會(huì)自動(dòng)擴(kuò)容,HashMap默認(rèn)使用的負(fù)載因子值為0.75f(當(dāng)容量達(dá)到四分之三進(jìn)行再散列(擴(kuò)容))。當(dāng)負(fù)載因子越大的時(shí)候能夠容納的鍵值對(duì)就越多但是查找的代價(jià)也會(huì)越高。
4、常用方法
4.1、put方法
在使用默認(rèn)構(gòu)造器初始化一個(gè)HashMap對(duì)象的時(shí)候,首次Put鍵值對(duì)的時(shí)候會(huì)先計(jì)算對(duì)應(yīng)Key的hash值通過hash值來確定存放的地址。
原理:
計(jì)算哈希值:
- 首先,根據(jù)鍵的
hashCode()方法計(jì)算出鍵的哈希值,并通過某種哈希算法對(duì)其進(jìn)行整合,以找到適合當(dāng)前HashMap容量的索引。
關(guān)于更多的哈希值的介紹可參考:HashMap中哈希值與數(shù)組坐標(biāo)的關(guān)聯(lián)
檢查桶(bucket):
- 使用計(jì)算出的索引在內(nèi)部數(shù)組
table中查找對(duì)應(yīng)的桶。如果該桶已經(jīng)有元素,則需要檢查是否存在哈希沖突(即多個(gè)鍵經(jīng)過哈希計(jì)算后映射到了同一個(gè)桶)。
處理沖突:
- 如果找到了沖突,
HashMap將采用鏈表(Java 7及之前)或紅黑樹(Java 8引入,當(dāng)鏈表長度超過一定閾值時(shí))來存儲(chǔ)沖突的元素。 - 遍歷桶中的元素,看是否已有相同的鍵。如果找到了,更新其對(duì)應(yīng)的值;如果沒找到,就將新鍵值對(duì)添加到鏈表或樹的末尾。
更新大小和閾值:
- 在插入后更新當(dāng)前的元素?cái)?shù)量(
size),并檢查是否需要擴(kuò)容(即判斷當(dāng)前數(shù)量與負(fù)載因子乘積的關(guān)系)
每當(dāng)元素?cái)?shù)量增加到閾值時(shí)(即 size >= threshold),HashMap 會(huì)調(diào)用 resize() 方法來擴(kuò)大底層數(shù)組的大小。
以下是put的簡化版代碼示例:
import java.util.Arrays;
class MyHashMap<K, V> {
static class Node<K, V> {
final int hash; // 存儲(chǔ)哈希值
final K key; // 存儲(chǔ)鍵
V value; // 存儲(chǔ)值
Node<K, V> next; // 指向下一個(gè)節(jié)點(diǎn)
Node(int hash, K key, V value, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
private Node<K, V>[] table; // 存儲(chǔ)節(jié)點(diǎn)的數(shù)組
private int size; // 當(dāng)前大小
private int threshold; // 擴(kuò)容閾值
private float loadFactor; // 負(fù)載因子
public MyHashMap(int initialCapacity, float loadFactor) {
this.loadFactor = loadFactor;
this.table = new Node[initialCapacity];
this.threshold = (int)(initialCapacity * loadFactor);
}
public void put(K key, V value) {
int hash = key == null ? 0 : key.hashCode();
int index = (hash & (table.length - 1)); // 計(jì)算桶的索引
Node<K, V> existingNode = table[index];
// 檢查桶是否為空
if (existingNode == null) {
// 如果桶為空,創(chuàng)建一個(gè)新節(jié)點(diǎn)
table[index] = new Node<>(hash, key, value, null);
size++;
} else {
// 遍歷鏈表,檢查是否已有相同的鍵
Node<K, V> currentNode = existingNode;
while (true) {
if (currentNode.hash == hash && (currentNode.key == key || (key != null && key.equals(currentNode.key)))) {
// 找到相同的鍵,更新值
currentNode.value = value;
return;
}
if (currentNode.next == null) {
// 沒有找到,添加新節(jié)點(diǎn)
currentNode.next = new Node<>(hash, key, value, null);
size++;
break;
}
currentNode = currentNode.next;
}
}
// 檢查是否需要擴(kuò)容
if (size >= threshold) {
resize();
}
}
private void resize() {
// 擴(kuò)容邏輯,目前省略具體實(shí)現(xiàn)
}
}4.2、get方法
擴(kuò)展:
在 Java 中,hashCode() 函數(shù)的返回值是一個(gè) int 類型,意味著哈希碼的值占用 32 位(4 字節(jié))。hashCode() 函數(shù)計(jì)算出的值可以在范圍 [-2,147,483,648, 2,147,483,647] 之間。
高位與低位的解釋
- 低位(Least Significant Bits, LSB):在這個(gè)上下文中,低位是指哈希碼的最右邊的位。對(duì)于一個(gè)
int類型,32 位中最右邊的 16 位即為低位部分(即得到的結(jié)果是0x0000FFFF)。 - 高位(Most Significant Bits, MSB):這是指哈希碼的最左邊的位。對(duì)于一個(gè)
int類型,最左邊的 16 位即為高位部分(即得到的結(jié)果是0xFFFF0000)。
先右移異或
首先會(huì)根據(jù)hashcode函數(shù)的到hash值,然后右移16位,然后將原始值和右移后的值進(jìn)行異或(相同為0,不同為1)。
int hash = key.hashCode(); // 分兩次右移,并進(jìn)行異或,減少碰撞 hash ^= (hash >>> 16);
為什么右移16位和異或:
- 右移 16 位的操作是為了將高位(通常低位 bits 可能更有可能重復(fù))與低位進(jìn)行混合。這是一種減小碰撞的方式,通過與高位進(jìn)行異或運(yùn)算,使得哈希值更加復(fù)雜,概率上減少哈希沖突。
- 右移之后的結(jié)果與原始哈希碼進(jìn)行異或,進(jìn)一步打散哈希值。這樣,無論哈希碼的某部分和另外一部分如何重復(fù),通過這一步都會(huì)產(chǎn)生不同的結(jié)果,減少哈希沖突的概率。
再計(jì)算哈希值:
- 使用鍵的
hashCode()方法計(jì)算哈希值,然后通過與當(dāng)前數(shù)組長度的模運(yùn)算(通常為數(shù)組長度減去 1)來找到該鍵應(yīng)存儲(chǔ)在數(shù)組中的桶(bucket)位置。
int index = (hash & (table.length - 1));
查找桶(bucket):
根據(jù)計(jì)算出的索引,從內(nèi)部數(shù)組 table 中獲取相應(yīng)的桶(Node[])。如果桶是空的,直接返回 null,表示所查詢的鍵不存在。
遍歷鏈表或紅黑樹 (處理哈希沖突):
如果桶中有多個(gè)元素(即形成了鏈表或紅黑樹),則需要遍歷鏈表或樹來查找對(duì)應(yīng)的鍵。
使用 equals() 方法比較每個(gè)節(jié)點(diǎn)的鍵,找到匹配的鍵時(shí)返回對(duì)應(yīng)的值。
返回值:
如果找到了匹配的鍵,返回其對(duì)應(yīng)的值;如果未找到,則返回 null。
以下是get方法的代碼示例圖:
class Node<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 class MyHashMap<K, V> {
private Node<K, V>[] table; // 存儲(chǔ)節(jié)點(diǎn)的數(shù)組
private int size;
public MyHashMap(int initialCapacity) {
this.table = new Node[initialCapacity];
}
public V get(K key) {
if (key == null) {
return null; // 處理空鍵
}
int hash = key.hashCode(); // 計(jì)算哈希值
int index = (hash & (table.length - 1)); // 計(jì)算索引
Node<K, V> node = table[index]; // 獲取對(duì)應(yīng)的桶
// 遍歷鏈表或紅黑樹
while (node != null) {
if (node.hash == hash && (node.key.equals(key))) {
return node.value; // 找到鍵,返回對(duì)應(yīng)的值
}
node = node.next; // 移動(dòng)到下一個(gè)節(jié)點(diǎn)
}
return null; // 未找到
}
}小結(jié):
時(shí)間復(fù)雜度:
get操作的平均時(shí)間復(fù)雜度為 O(1),在理想情況下,若沒有發(fā)生沖突。- 在最壞的情況下(所有元素都發(fā)生沖突,組成鏈表或紅黑樹),時(shí)間復(fù)雜度為 O(n) 或 O(log n),分別取決于鏈表或樹的實(shí)現(xiàn)方式。
處理哈希沖突:
- 若兩個(gè)或多個(gè)鍵的哈希值相同,會(huì)發(fā)生哈希沖突,使用鏈表或紅黑樹來解決沖突。
5、擴(kuò)容
關(guān)于HashMap的原理圖。

主要用于存儲(chǔ)鍵值對(duì)。其內(nèi)部實(shí)現(xiàn)包含了數(shù)組和鏈表(或紅黑樹)以處理哈希沖突。HashMap 在使用過程中會(huì)發(fā)生擴(kuò)容,以確保其存儲(chǔ)性能及均勻分布。
下面是 HashMap 擴(kuò)容的詳細(xì)原理。
5.1、擴(kuò)容介紹
HashMap 的擴(kuò)容是指當(dāng) HashMap 的當(dāng)前容量不足以存儲(chǔ)新的鍵值對(duì)時(shí),自動(dòng)調(diào)整其容量和結(jié)構(gòu),以提高其性能。
擴(kuò)容通常會(huì)在 put() 方法中觸發(fā),這個(gè)方法用于將鍵值對(duì)添加到哈希表中。
5.2、擴(kuò)容的觸發(fā)條件
擴(kuò)容會(huì)在以下情況下觸發(fā):
- 當(dāng)
HashMap中的元素?cái)?shù)量超過了負(fù)載因子(load factor)與當(dāng)前容量的乘積,即:threshold=capacity×load 。
默認(rèn)為 0.75 的負(fù)載因子。
舉個(gè)例子:
- 初始容量: 默認(rèn)
HashMap初始容量為 16。 - 負(fù)載因子: 默認(rèn)負(fù)載因子為 0.75。
- 閾值: 計(jì)算得出的閾值為 16×0.75=12。
所以,當(dāng)插入的元素?cái)?shù)量達(dá)到 12 時(shí),HashMap 會(huì)觸發(fā)擴(kuò)容。
5.3、擴(kuò)容過程
當(dāng)需要進(jìn)行擴(kuò)容時(shí),HashMap 的擴(kuò)容過程如下:
當(dāng)前容量翻倍:
- 新容量是當(dāng)前容量的 2 倍。
重新計(jì)算哈希:
- 每個(gè)鍵的哈希值會(huì)基于新的容量重新計(jì)算,并通過新的桶數(shù)組插入到相應(yīng)的桶中。
- 為了保持元素的分布處理,
HashMap使用“鏈地址法”或拉鏈法處理哈希沖突。
元素遷移:
- 在擴(kuò)容過程中,所有的鍵值對(duì)會(huì)被重新計(jì)算位置并拷貝到新的桶數(shù)組。
- 這需要遍歷當(dāng)前所有的鏈表或紅黑樹并重新插入到新位置。
示例:
public class HashMapDemo {
public static void main(String[] args) {
MyHashMap<String, Integer> map = new MyHashMap<>(4, 0.75f); // 初始化容量為4,負(fù)載因子為0.75
map.put("one", 1); → 當(dāng)前大小為 1(沒有觸發(fā)擴(kuò)容)。
map.put("two", 2); → 當(dāng)前大小為 2(沒有觸發(fā)擴(kuò)容)。
map.put("three", 3); → 當(dāng)前大小為 3(這時(shí)已達(dá)到閾值,因此將觸發(fā)擴(kuò)容)。
map.put("four", 4); → 由于已經(jīng)擴(kuò)容,實(shí)際中會(huì)發(fā)生元素的重新分配,現(xiàn)在當(dāng)前數(shù)組的大小為 4。
map.put("five", 5); → 當(dāng)前大小為 5 (但是不會(huì)再觸發(fā)擴(kuò)容,因?yàn)閿?shù)組容量已增加到 8)。
// 你可以擴(kuò)展上面代碼來實(shí)現(xiàn) get 方法等
}
}HashMap是先插入還是先擴(kuò)容:HashMap是先插入數(shù)據(jù)再進(jìn)行擴(kuò)容的,但是如果是剛剛初始化容器的時(shí)候是先擴(kuò)容再插入數(shù)據(jù)。
5.4、擴(kuò)容失敗
通常會(huì)導(dǎo)致 OutOfMemoryError 異常,當(dāng)前的操作(例如添加元素)將會(huì)失敗。之后,HashMap 仍然保持不變,已經(jīng)加入的元素也不會(huì)被丟失。
擴(kuò)容屬于原子操作,不會(huì)重試,因?yàn)閿U(kuò)容是同步操作,失敗意味著資源不足,無法繼續(xù)。
當(dāng)擴(kuò)容成功后的代碼示例如下:
package com.example.test;
import java.lang.reflect.Field;
import java.util.HashMap;
public class HashMapTest {
public static void main(String[] args) {
HashMap<String, Object> map = new HashMap<>(4,0.75f);
map.put("one",1);
map.put("two",2);
map.put("three",3);
map.put("four",4);
try {
// 獲取底層數(shù)組長度
Field tableField = HashMap.class.getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(map);
System.out.println("鍵值對(duì)數(shù)量(map.size): " + map.size());
System.out.println("底層數(shù)組長度: " + (table != null ? table.length : 0));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
System.out.println("map.size="+map.size());
}
}結(jié)果輸出:
鍵值對(duì)數(shù)量(map.size): 4
底層數(shù)組長度: 8
map.size=4
6、迭代異常
迭代器的remove()可以安全使用,而直接操作Map會(huì)觸發(fā)異常。通過fast-fail機(jī)制可以有效的進(jìn)行檢測。
代碼示例如下:
// 正確方式(使用迭代器的remove)
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
if (it.next().equals("remove")) {
it.remove(); // 不會(huì)增加modCount
}
} 這個(gè)是正常的, Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
map.put("C", 3); // 這里會(huì)觸發(fā)fail-fast
} 而這個(gè)會(huì)觸發(fā)fail-fast6.1、區(qū)別對(duì)比

6.2、源碼解析
1. 迭代器remove實(shí)現(xiàn)
// HashMap.HashIterator.remove()
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount; // 關(guān)鍵點(diǎn):同步更新!
}關(guān)鍵步驟:
- 檢查合法性
- 實(shí)際刪除節(jié)點(diǎn)
- 同步更新expectedModCount
2. 直接put操作
// HashMap.put()
public V put(K key, V value) {
// ...省略其他代碼...
++modCount; // 修改計(jì)數(shù)器增加
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}此時(shí)expectedModCount不會(huì)更新,導(dǎo)致下次迭代檢查時(shí):
// HashIterator.nextNode()
final Node<K,V> nextNode() {
if (modCount != expectedModCount) // 此時(shí)不相等!
throw new ConcurrentModificationException();
// ...
}6.3、結(jié)構(gòu)圖
比較expectedModCount和ModCount值是否相等。通常在hashmap的clear(),remove(),put()方法里面都會(huì)進(jìn)行modcount++;
如下圖所示:

6.4、線程安全
即使單線程環(huán)境,兩種操作也有本質(zhì)區(qū)別:
迭代器remove:
- 操作路徑:迭代器→Map
- 迭代器全程掌握修改控制權(quán)
直接修改Map:
- 操作路徑:直接訪問Map
- 迭代器無法感知外部修改
6.5、開發(fā)建議
1. 安全刪除模式
// 安全刪除示例
List<String> toRemove = new ArrayList<>();
for (Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue() < 0) {
toRemove.add(entry.getKey());
}
}
toRemove.forEach(map::remove); // 迭代完成后再批量刪除2. 多線程替代方案
// 使用ConcurrentHashMap
ConcurrentHashMap<String, Integer> safeMap = new ConcurrentHashMap<>();
// 迭代時(shí)修改不會(huì)拋異常
for (String key : safeMap.keySet()) {
if (key.startsWith("test")) {
safeMap.remove(key); // 安全操作
}
}6.6、底層設(shè)計(jì)
契約式設(shè)計(jì):
- 迭代器與Map約定:只有通過迭代器的修改才被認(rèn)可
防御式編程:
- 假設(shè)外部修改都可能破壞一致性
- 通過fail-fast快速暴露問題
最小驚訝原則:
- 直接修改集合產(chǎn)生異常符合開發(fā)者直覺
- 迭代器提供專用修改方法更安全
這種設(shè)計(jì)確保了集合在迭代過程中的狀態(tài)可控性,雖然限制了靈活性,但大幅提高了代碼的可靠性。理解這一機(jī)制可以幫助開發(fā)者避免常見的并發(fā)修改錯(cuò)誤。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java 數(shù)據(jù)結(jié)構(gòu)中棧和隊(duì)列的實(shí)例詳解
這篇文章主要介紹了java 數(shù)據(jù)結(jié)構(gòu)中棧和隊(duì)列的實(shí)例詳解的相關(guān)資料,主要使用數(shù)組與線性表的方法來實(shí)現(xiàn),需要的朋友可以參考下2017-09-09
MybatisPlus的LambdaQueryWrapper用法詳解
LambdaQueryWrapper<Tag>?是 MyBatis-Plus 框架中的一個(gè)功能強(qiáng)大的查詢構(gòu)造器,它用于構(gòu)建 SQL 查詢條件,具有一定的參考價(jià)值,感興趣的可以了解一下2024-10-10
在 Spring Boot 中實(shí)現(xiàn)異常處理最佳實(shí)踐
本文介紹如何在Spring Boot中實(shí)現(xiàn)異常處理,涵蓋核心概念、實(shí)現(xiàn)方法、與先前查詢的集成、性能分析、常見問題和最佳實(shí)踐,感興趣的朋友一起看看吧2025-04-04
IDEA中g(shù)it使用實(shí)現(xiàn)文件修改后有顏色提示的設(shè)置
本文介紹了IntelliJ IDEA中SVN插件的可視化設(shè)置,說明默認(rèn)只有修改文件有顏色提示,需手動(dòng)設(shè)置File->setting->VersionControl圖示以顯示父目錄顏色;可可設(shè)置不同狀態(tài)下的高亮顏色,并可提供其他設(shè)置選項(xiàng)2026-04-04
Springboot 整合通用mapper和pagehelper展示分頁數(shù)據(jù)的問題(附github源碼)
這篇文章主要介紹了Springboot 整合通用mapper和pagehelper展示分頁數(shù)據(jù)(附github源碼),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-09-09

