對(duì)HashMap的數(shù)據(jù)結(jié)構(gòu)的學(xué)習(xí)解讀
1、介紹
JDK1.8之前HashMap由數(shù)組+鏈表組成。數(shù)組是HshMap的主體,鏈表則是主要為了解決哈希沖突。
JDK1.8及以后的版本在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長(zhǎng)度大于閾值(或者紅黑樹的邊界值,默認(rèn)為8,且數(shù)組中桶的數(shù)量大于64,此時(shí)此索引位置上的所有數(shù)據(jù)改為使用紅黑樹存儲(chǔ)。
1.1、數(shù)據(jù)結(jié)構(gòu)
如下圖所示:
桶數(shù)組是用來(lái)存儲(chǔ)數(shù)據(jù)元素,鏈表是用來(lái)解決沖突,紅黑樹是為了提高查詢的效率。
數(shù)據(jù)元素通過(guò)映射關(guān)系,也就是散列函數(shù),映射到桶數(shù)組對(duì)應(yīng)索引的位置。
1.如果發(fā)生沖突,從沖突的位置拉一個(gè)鏈表,插入沖突的元素。
- 如果鏈表長(zhǎng)度>8&數(shù)組大小>=64,鏈表轉(zhuǎn)為紅黑樹。
- 如果紅黑樹節(jié)點(diǎn)個(gè)數(shù)<6 ,轉(zhuǎn)為鏈表。
2.但是數(shù)組長(zhǎng)度小于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)行左旋,右旋,變色這些操作來(lái)保持平衡 。同時(shí)數(shù)組長(zhǎng)度小于64時(shí),搜索時(shí)間相對(duì)要快些。
所以綜上所述為了提高性能和減少搜索時(shí)間,底層在閾值大于8并且數(shù)組長(zhǎng)度大于64時(shí),鏈表才轉(zhuǎn)換為紅黑樹。
至于為什么要轉(zhuǎn)變從鏈表轉(zhuǎn)到紅黑樹,以及紅黑樹轉(zhuǎn)鏈表:JDK8 HashMap紅黑樹退化為鏈表的機(jī)制解讀
1.2、紅黑樹
為什么要使用紅黑樹:
如果鏈表非常長(zhǎng)(特別是在某些不均勻分布的哈希情況下),查找、插入和刪除的性能會(huì)降到 O(n) 的水平,因?yàn)樾枰闅v整個(gè)鏈表。
在這里簡(jiǎn)單介紹下紅黑樹:
由上圖可知:
紅黑樹本質(zhì)上是一種二叉查找樹,為了保持平衡,它又在二叉查找樹的基礎(chǔ)上增加了一些規(guī)則:
1、每個(gè)節(jié)點(diǎn)要么是紅色,要么是黑色;
2、根節(jié)點(diǎn)永遠(yuǎn)是黑色的;
3、所有的葉子節(jié)點(diǎn)都是是黑色的(注意這里說(shuō)葉子節(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: 鏈表長(zhǎng)度大于等于該參數(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)鏈表長(zhǎ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 通過(guò)取鍵的哈希值并與數(shù)組的長(zhǎng)度進(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ù)量超過(guò)這個(gè)比率所計(jì)算的閾值時(shí),它將擴(kuò)容。
通過(guò)合理的負(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ù)量 // 測(cè)試負(fù)載因子為默認(rèn)值 (0.75) long timeWithDefaultLoadFactor = testHashMapPerformance(0.75f, numberOfElements); System.out.println("Time taken with default load factor (0.75): " + timeWithDefaultLoadFactor + " ms"); // 測(cè)試負(fù)載因子為 0.5 long timeWithLowLoadFactor = testHashMapPerformance(0.5f, numberOfElements); System.out.println("Time taken with low load factor (0.5): " + timeWithLowLoadFactor + " ms"); // 測(cè)試負(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ī)制,能夠檢測(cè)到在迭代 HashMap 時(shí)是否發(fā)生了并發(fā)修改,通過(guò)與迭代器中的記錄值進(jìn)行比較來(lái)判斷。
關(guān)于fail-fast機(jī)制:可參考JDK8 HashMap紅黑樹退化為鏈表的機(jī)制解析
3、構(gòu)造方法
以下有四種不同的初始化方式。
1、無(wú)參構(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值通過(guò)hash值來(lái)確定存放的地址。
原理:
計(jì)算哈希值:
- 首先,根據(jù)鍵的
hashCode()
方法計(jì)算出鍵的哈希值,并通過(guò)某種哈希算法對(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)過(guò)哈希計(jì)算后映射到了同一個(gè)桶)。
處理沖突:
- 如果找到了沖突,
HashMap
將采用鏈表(Java 7及之前)或紅黑樹(Java 8引入,當(dāng)鏈表長(zhǎng)度超過(guò)一定閾值時(shí))來(lái)存儲(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()
方法來(lái)擴(kuò)大底層數(shù)組的大小。
以下是put的簡(jiǎn)化版代碼示例:
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)行混合。這是一種減小碰撞的方式,通過(guò)與高位進(jìn)行異或運(yùn)算,使得哈希值更加復(fù)雜,概率上減少哈希沖突。
- 右移之后的結(jié)果與原始哈希碼進(jìn)行異或,進(jìn)一步打散哈希值。這樣,無(wú)論哈希碼的某部分和另外一部分如何重復(fù),通過(guò)這一步都會(huì)產(chǎn)生不同的結(jié)果,減少哈希沖突的概率。
再計(jì)算哈希值:
- 使用鍵的
hashCode()
方法計(jì)算哈希值,然后通過(guò)與當(dāng)前數(shù)組長(zhǎng)度的模運(yùn)算(通常為數(shù)組長(zhǎng)度減去 1)來(lái)找到該鍵應(yīng)存儲(chǔ)在數(shù)組中的桶(bucket)位置。
int index = (hash & (table.length - 1));
查找桶(bucket):
根據(jù)計(jì)算出的索引,從內(nèi)部數(shù)組 table
中獲取相應(yīng)的桶(Node[])。如果桶是空的,直接返回 null
,表示所查詢的鍵不存在。
遍歷鏈表或紅黑樹 (處理哈希沖突):
如果桶中有多個(gè)元素(即形成了鏈表或紅黑樹),則需要遍歷鏈表或樹來(lái)查找對(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ā)生哈希沖突,使用鏈表或紅黑樹來(lái)解決沖突。
5、擴(kuò)容
關(guān)于HashMap的原理圖。
主要用于存儲(chǔ)鍵值對(duì)。其內(nèi)部實(shí)現(xiàn)包含了數(shù)組和鏈表(或紅黑樹)以處理哈希沖突。HashMap
在使用過(guò)程中會(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ù)量超過(guò)了負(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ò)容過(guò)程
當(dāng)需要進(jìn)行擴(kuò)容時(shí),HashMap
的擴(kuò)容過(guò)程如下:
當(dāng)前容量翻倍:
- 新容量是當(dāng)前容量的 2 倍。
重新計(jì)算哈希:
- 每個(gè)鍵的哈希值會(huì)基于新的容量重新計(jì)算,并通過(guò)新的桶數(shù)組插入到相應(yīng)的桶中。
- 為了保持元素的分布處理,
HashMap
使用“鏈地址法”或拉鏈法處理哈希沖突。
元素遷移:
- 在擴(kuò)容過(guò)程中,所有的鍵值對(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ò)展上面代碼來(lái)實(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ò)容是同步操作,失敗意味著資源不足,無(wú)法繼續(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ù)組長(zhǎng)度 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ù)組長(zhǎng)度: " + (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ù)組長(zhǎng)度: 8
map.size=4
6、迭代異常
迭代器的remove()可以安全使用,而直接操作Map會(huì)觸發(fā)異常。通過(guò)fast-fail機(jī)制可以有效的進(jìn)行檢測(cè)。
代碼示例如下:
// 正確方式(使用迭代器的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-fast
6.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
- 迭代器無(wú)法感知外部修改
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約定:只有通過(guò)迭代器的修改才被認(rèn)可
防御式編程:
- 假設(shè)外部修改都可能破壞一致性
- 通過(guò)fail-fast快速暴露問題
最小驚訝原則:
- 直接修改集合產(chǎn)生異常符合開發(fā)者直覺
- 迭代器提供專用修改方法更安全
這種設(shè)計(jì)確保了集合在迭代過(guò)程中的狀態(tài)可控性,雖然限制了靈活性,但大幅提高了代碼的可靠性。理解這一機(jī)制可以幫助開發(fā)者避免常見的并發(fā)修改錯(cuò)誤。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
IDEA中HTML通過(guò)servlet3.0注解名提交表單到servlet類找不到頁(yè)面的問題
這篇文章主要介紹了IDEA中HTML通過(guò)servlet3.0注解名提交表單到servlet類找不到頁(yè)面的問題,本文通過(guò)場(chǎng)景描述及問題解析,給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07idea2023遠(yuǎn)程調(diào)試springboot的過(guò)程詳解
這篇文章主要介紹了idea2023遠(yuǎn)程調(diào)試,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-08-08Java如何基于ProcessBuilder類調(diào)用外部程序
這篇文章主要介紹了Java如何基于ProcessBuilder類調(diào)用外部程序,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01Java concurrency線程池之線程池原理(一)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Java concurrency線程池之線程池原理,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06Spring Boot處理全局統(tǒng)一異常的兩種方法與區(qū)別
這篇文章主要給大家介紹了關(guān)于Spring Boot處理全局統(tǒng)一異常的兩種方法與區(qū)別,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06解決spring-boot 打成jar包后 啟動(dòng)時(shí)指定參數(shù)無(wú)效的問題
這篇文章主要介紹了解決spring-boot 打成jar包后 啟動(dòng)時(shí)指定參數(shù)無(wú)效的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06java如何實(shí)現(xiàn)判斷文件的真實(shí)類型
本篇文章主要介紹了java如何實(shí)現(xiàn)判斷文件的真實(shí)類型,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08JAVA實(shí)現(xiàn)社會(huì)統(tǒng)一信用代碼校驗(yàn)的方法
這篇文章主要介紹了JAVA實(shí)現(xiàn)社會(huì)統(tǒng)一信用代碼校驗(yàn)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07Spring Cloud中FeignClient實(shí)現(xiàn)文件上傳功能
這篇文章主要為大家詳細(xì)介紹了Spring Cloud中FeignClient實(shí)現(xiàn)文件上傳功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-04-04