Java?HashMap從源碼到核心機(jī)制實(shí)現(xiàn)原理深度解析
前言
作為Java開發(fā)中最常用的集合類之一,HashMap以其高效的鍵值對(duì)存取能力成為日常開發(fā)的“標(biāo)配”,但多數(shù)開發(fā)者僅停留在“會(huì)用”層面,對(duì)其底層實(shí)現(xiàn)、擴(kuò)容機(jī)制、線程安全等核心問(wèn)題一知半解。本文將從數(shù)據(jù)結(jié)構(gòu)、核心機(jī)制、源碼解析三個(gè)維度,徹底拆解HashMap的實(shí)現(xiàn)原理,結(jié)合JDK 8的核心優(yōu)化點(diǎn),幫你從“使用”走向“理解”。
一、HashMap核心定位與設(shè)計(jì)目標(biāo)
HashMap是基于哈希表實(shí)現(xiàn)的Map接口實(shí)現(xiàn)類,核心特點(diǎn):
- 允許
key和value為null(Hashtable不允許); - 無(wú)序(存儲(chǔ)順序與插入順序無(wú)關(guān));
- JDK 8前采用“數(shù)組+鏈表”,JDK 8引入“紅黑樹”優(yōu)化鏈表過(guò)長(zhǎng)問(wèn)題;
- 非線程安全(多線程操作可能導(dǎo)致死循環(huán)、數(shù)據(jù)丟失);
- 查找、插入、刪除的平均時(shí)間復(fù)雜度為
O(1),最壞情況(哈希沖突嚴(yán)重)JDK 7為O(n),JDK 8優(yōu)化為O(logn)。
二、HashMap核心數(shù)據(jù)結(jié)構(gòu)
1. 基礎(chǔ)結(jié)構(gòu):數(shù)組(桶)+ 鏈表 + 紅黑樹
HashMap的底層核心是哈希桶數(shù)組(Node[] table),每個(gè)數(shù)組元素(桶)對(duì)應(yīng)一個(gè)鏈表/紅黑樹,用于解決哈希沖突:
- 哈希桶數(shù)組:存儲(chǔ)數(shù)據(jù)的核心容器,默認(rèn)初始容量為16(
DEFAULT_INITIAL_CAPACITY); - 鏈表:當(dāng)多個(gè)key的哈希值映射到同一個(gè)桶時(shí),通過(guò)鏈表串聯(lián)(JDK 7頭插法,JDK 8尾插法,解決并發(fā)死循環(huán)問(wèn)題);
- 紅黑樹:當(dāng)鏈表長(zhǎng)度≥8且數(shù)組容量≥64時(shí),鏈表轉(zhuǎn)為紅黑樹(鏈表長(zhǎng)度≤6時(shí)回退為鏈表),降低查詢耗時(shí)。
2. 核心節(jié)點(diǎn)類
JDK 8中HashMap的節(jié)點(diǎn)分為兩種:
// 普通鏈表節(jié)點(diǎn)
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // key的哈希值(經(jīng)過(guò)擾動(dòng)處理)
final K key; // 鍵
V value; // 值
Node<K,V> next; // 下一個(gè)節(jié)點(diǎn)引用
Node(int hash, K key, V value, Node<K,V> next) { ... }
}
// 紅黑樹節(jié)點(diǎn)(繼承自Node)
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父節(jié)點(diǎn)
TreeNode<K,V> left; // 左子節(jié)點(diǎn)
TreeNode<K,V> right; // 右子節(jié)點(diǎn)
TreeNode<K,V> prev; // 前驅(qū)節(jié)點(diǎn)
boolean red; // 紅黑樹顏色標(biāo)記
TreeNode(int hash, K key, V value, Node<K,V> next) { ... }
}
三、HashMap核心機(jī)制解析
1. 哈希計(jì)算與尋址:如何定位key的存儲(chǔ)位置
HashMap的核心是通過(guò)哈希算法將key映射到數(shù)組的指定位置,分為兩步:
(1)哈希值計(jì)算(擾動(dòng)函數(shù))
為了減少哈希沖突,JDK 8對(duì)key的hashCode()進(jìn)行“擾動(dòng)處理”,混合高位和低位特征:
static final int hash(Object key) {
int h;
// key為null時(shí)hash為0,所以HashMap允許key為null
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 取key的
hashCode()(32位整數(shù)); - 將高16位與低16位異或(
^),讓高位特征參與尋址,降低哈希沖突概率。
(2)數(shù)組尋址
通過(guò)哈希值計(jì)算key在數(shù)組中的索引:
// n為數(shù)組長(zhǎng)度(必須是2的冪) int index = (n - 1) & hash;
- 數(shù)組長(zhǎng)度
n設(shè)計(jì)為2的冪,使得n-1的二進(jìn)制全為1,等價(jià)于hash % n但效率更高; - 若
n不是2的冪,(n-1) & hash會(huì)導(dǎo)致部分索引無(wú)法命中,浪費(fèi)數(shù)組空間。
2. 擴(kuò)容機(jī)制(resize())
當(dāng)HashMap的元素?cái)?shù)量(size)超過(guò)負(fù)載因子×數(shù)組容量時(shí),觸發(fā)擴(kuò)容,核心規(guī)則:
- 負(fù)載因子默認(rèn)值:0.75(
DEFAULT_LOAD_FACTOR),平衡空間利用率和哈希沖突; - 擴(kuò)容規(guī)則:數(shù)組容量翻倍(2倍),重新計(jì)算所有節(jié)點(diǎn)的索引并遷移;
- 擴(kuò)容優(yōu)化(JDK 8):由于容量翻倍,節(jié)點(diǎn)新索引要么不變,要么為原索引+舊容量,無(wú)需重新計(jì)算哈希,提升擴(kuò)容效率。
擴(kuò)容核心邏輯(簡(jiǎn)化版源碼)
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; // 擴(kuò)容閾值(負(fù)載因子×容量)
int newCap, newThr = 0;
if (oldCap > 0) {
// 超過(guò)最大容量(2^30),不再擴(kuò)容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 容量翻倍,閾值也翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
newThr = oldThr << 1;
}
}
// 初始化容量(首次put時(shí))
else if (oldThr > 0) newCap = oldThr;
else {
newCap = DEFAULT_INITIAL_CAPACITY; // 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 12
}
// 創(chuàng)建新數(shù)組,遷移舊節(jié)點(diǎn)
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 單個(gè)節(jié)點(diǎn),直接遷移
if (e.next == null) newTab[e.hash & (newCap - 1)] = e;
// 紅黑樹節(jié)點(diǎn),拆分遷移
else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 鏈表節(jié)點(diǎn),按新索引拆分(JDK 8優(yōu)化點(diǎn))
else {
Node<K,V> loHead = null, loTail = null; // 索引不變的節(jié)點(diǎn)
Node<K,V> hiHead = null, hiTail = null; // 索引=原索引+舊容量的節(jié)點(diǎn)
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 { // 索引=j+oldCap
if (hiTail == null) hiHead = e;
else hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
3. 紅黑樹轉(zhuǎn)換規(guī)則
JDK 8引入紅黑樹的核心目的是解決“鏈表過(guò)長(zhǎng)導(dǎo)致查詢效率低”的問(wèn)題,轉(zhuǎn)換條件嚴(yán)格:
- 鏈表轉(zhuǎn)紅黑樹:
- 鏈表長(zhǎng)度≥8;
- 數(shù)組容量≥64(若數(shù)組容量<64,先擴(kuò)容而非轉(zhuǎn)紅黑樹);
- 紅黑樹轉(zhuǎn)鏈表:鏈表長(zhǎng)度≤6(避免頻繁轉(zhuǎn)換);
- 閾值設(shè)計(jì)原因:基于泊松分布,鏈表長(zhǎng)度≥8的概率僅0.00000006,幾乎是小概率事件,避免過(guò)度優(yōu)化。
四、核心方法源碼解析:put()
put方法是HashMap最核心的方法,完整體現(xiàn)了“哈希計(jì)算→尋址→沖突處理→擴(kuò)容”的全流程,JDK 8核心邏輯:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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. 數(shù)組未初始化/長(zhǎng)度為0,先擴(kuò)容
if ((tab = table) == null || (n = tab.length) == 0) {
n = (tab = resize()).length;
}
// 2. 計(jì)算索引,若桶為空,直接創(chuàng)建新節(jié)點(diǎn)
if ((p = tab[i = (n - 1) & hash]) == null) {
tab[i] = newNode(hash, key, value, null);
} else {
Node<K,V> e; K k;
// 3. 桶中節(jié)點(diǎn)的key與當(dāng)前key相同,直接覆蓋value
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
e = p;
}
// 4. 桶中是紅黑樹節(jié)點(diǎn),調(diào)用紅黑樹插入方法
else if (p instanceof TreeNode) {
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
}
// 5. 桶中是鏈表節(jié)點(diǎn),遍歷鏈表
else {
for (int binCount = 0; ; ++binCount) {
// 鏈表尾部,插入新節(jié)點(diǎn)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 鏈表長(zhǎng)度≥8,觸發(fā)紅黑樹轉(zhuǎn)換
if (binCount >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, hash);
}
break;
}
// 找到相同key,跳出循環(huán)(后續(xù)覆蓋value)
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
break;
}
p = e;
}
}
// 6. 存在相同key,覆蓋value并返回舊值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) {
e.value = value;
}
afterNodeAccess(e); // 空方法,LinkedHashMap重寫
return oldValue;
}
}
++modCount; // 快速失?。╢ail-fast)標(biāo)記
// 7. 元素?cái)?shù)量超過(guò)閾值,觸發(fā)擴(kuò)容
if (++size > threshold) {
resize();
}
afterNodeInsertion(evict); // 空方法,LinkedHashMap重寫
return null;
}
五、HashMap的線程安全問(wèn)題
1. 核心問(wèn)題
HashMap非線程安全,多線程并發(fā)操作會(huì)導(dǎo)致:
- JDK 7死循環(huán):擴(kuò)容時(shí)頭插法導(dǎo)致鏈表成環(huán),查詢時(shí)無(wú)限循環(huán);
- 數(shù)據(jù)丟失/覆蓋:多線程同時(shí)put,可能導(dǎo)致節(jié)點(diǎn)覆蓋;
- 擴(kuò)容丟失數(shù)據(jù):多線程擴(kuò)容時(shí),節(jié)點(diǎn)遷移過(guò)程中數(shù)據(jù)丟失。
2. 替代方案
- ConcurrentHashMap:JDK 8采用“CAS+分段鎖”實(shí)現(xiàn)線程安全,性能遠(yuǎn)優(yōu)于Hashtable;
- Collections.synchronizedMap:通過(guò)包裝類加全局鎖,性能較差;
- Hashtable:方法加
synchronized,全局鎖,性能最差(不推薦)。
六、實(shí)戰(zhàn)/面試高頻要點(diǎn)
1. 為什么HashMap的容量必須是2的冪?
- 尋址時(shí)
(n-1) & hash等價(jià)于hash % n,位運(yùn)算效率更高; - 擴(kuò)容時(shí)節(jié)點(diǎn)新索引僅兩種可能(原索引/原索引+舊容量),無(wú)需重新計(jì)算哈希,提升擴(kuò)容效率;
- 減少哈希沖突,讓索引分布更均勻。
2. 負(fù)載因子為什么默認(rèn)是0.75?
- 0.75是時(shí)間和空間的平衡值:
- 負(fù)載因子過(guò)高:哈希沖突概率增加,鏈表/紅黑樹變長(zhǎng),查詢效率降低;
- 負(fù)載因子過(guò)低:數(shù)組空間利用率低,擴(kuò)容頻繁,性能開銷大。
3. JDK 7 vs JDK 8 HashMap核心差異
| 特性 | JDK 7 | JDK 8 |
|---|---|---|
| 數(shù)據(jù)結(jié)構(gòu) | 數(shù)組+鏈表 | 數(shù)組+鏈表+紅黑樹 |
| 插入方式 | 頭插法(并發(fā)死循環(huán)) | 尾插法(解決死循環(huán)) |
| 哈希計(jì)算 | 4次位運(yùn)算+5次異或 | 1次異或(簡(jiǎn)化擾動(dòng)) |
| 擴(kuò)容后索引 | 重新計(jì)算 | 僅兩種可能(優(yōu)化效率) |
| 失敗機(jī)制 | fail-fast | fail-fast |
七、總結(jié)
HashMap的核心設(shè)計(jì)圍繞“高效哈希尋址”展開,JDK 8的紅黑樹優(yōu)化、尾插法、擴(kuò)容優(yōu)化等,都是為了在哈希沖突場(chǎng)景下保證性能:
- 數(shù)據(jù)結(jié)構(gòu):數(shù)組是基礎(chǔ),鏈表解決沖突,紅黑樹優(yōu)化長(zhǎng)鏈表;
- 核心機(jī)制:哈希擾動(dòng)減少?zèng)_突,2次冪容量提升尋址效率,0.75負(fù)載因子平衡時(shí)空;
- 線程安全:避免多線程直接操作,優(yōu)先使用ConcurrentHashMap;
- 實(shí)戰(zhàn)建議:初始化時(shí)指定容量(避免頻繁擴(kuò)容),key盡量用不可變類型(如String、Integer),保證hashCode穩(wěn)定。
理解HashMap的實(shí)現(xiàn)原理,不僅能應(yīng)對(duì)面試,更能在高并發(fā)、大數(shù)據(jù)量場(chǎng)景下合理使用HashMap,避免性能問(wèn)題和線上故障。
到此這篇關(guān)于Java HashMap從源碼到核心機(jī)制實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)Java HashMap實(shí)現(xiàn)原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決SpringMVC獲取請(qǐng)求參數(shù)亂碼問(wèn)題
在使用SpringMVC和thymeleaf進(jìn)行請(qǐng)求參數(shù)處理時(shí),可能會(huì)遇到亂碼問(wèn)題,對(duì)于GET方法亂碼,可通過(guò)修改Tomcat的server.xml文件,添加URIEncoding="UTF-8"解決,而POST方法亂碼,則需在web.xml配置SpringMVC提供的過(guò)濾器2024-11-11
Intellij Mybatis連接Mysql數(shù)據(jù)庫(kù)
最近在搞android的項(xiàng)目,在開發(fā)過(guò)程中遇到了好多問(wèn)題,今天小編給大家說(shuō)下mybatis連接MySQL數(shù)據(jù)庫(kù)的方法,感興趣的朋友跟著小編一起學(xué)習(xí)吧2016-10-10
Java設(shè)計(jì)模式中的裝飾器模式簡(jiǎn)析
這篇文章主要介紹了Java設(shè)計(jì)模式中的裝飾器模式簡(jiǎn)析,裝飾模式能夠?qū)崿F(xiàn)動(dòng)態(tài)的為對(duì)象添加功能,是從一個(gè)對(duì)象外部來(lái)給對(duì)象添加功能,通常給對(duì)象添加功能,要么直接修改對(duì)象添加相應(yīng)的功能,要么派生對(duì)應(yīng)的子類來(lái)擴(kuò)展,抑或是使用對(duì)象組合的方式,需要的朋友可以參考下2023-12-12
Spring超詳細(xì)講解創(chuàng)建BeanDefinition流程
Spring在初始化過(guò)程中,將xml中定義的對(duì)象解析到了BeanDefinition對(duì)象中,我們有必要了解一下BeanDefinition的內(nèi)部結(jié)構(gòu),有助于我們理解Spring的初始化流程2022-06-06
springboot如何開啟和關(guān)閉kafka消費(fèi)
在Kafka消費(fèi)者中,通過(guò)關(guān)閉自動(dòng)消費(fèi)配置,使用自定義容器工廠,并在消費(fèi)監(jiān)聽器上設(shè)置id,可以手動(dòng)控制消費(fèi)的開啟和關(guān)閉,這是根據(jù)個(gè)人經(jīng)驗(yàn)總結(jié)的方法,旨在幫助其他開發(fā)者2024-12-12
Java如何計(jì)算兩個(gè)時(shí)間段內(nèi)的工作日天數(shù)
這篇文章主要介紹了Java如何計(jì)算兩個(gè)時(shí)間段內(nèi)的工作日天數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
快速上手Mybatis-plus結(jié)構(gòu)構(gòu)建過(guò)程
這篇文章主要介紹了快速上手Mybatis-plus結(jié)構(gòu)構(gòu)建過(guò)程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07
解決Java變異出現(xiàn)錯(cuò)誤No enclosing instance of type XXX is accessible
這牌你文章主要給大家分享解決Java變異出現(xiàn)錯(cuò)誤,具體的饑餓絕方案請(qǐng)看下面文章的內(nèi)容,需要的朋友可以參考一下,希望能幫助到你2021-09-09

