Java Map常用方法和實(shí)現(xiàn)類(lèi)的核心原理


前言
在Java集合框架中,Map是最核心、最常用的數(shù)據(jù)結(jié)構(gòu)之一。與Collection體系下的List、Set不同,Map采用**鍵值對(duì)(Key-Value)**的存儲(chǔ)方式,每個(gè)鍵映射到一個(gè)值,鍵在同一個(gè)Map中不可重復(fù)。這種設(shè)計(jì)使得Map特別適合需要通過(guò)鍵快速查找值的場(chǎng)景,如緩存系統(tǒng)、配置管理、數(shù)據(jù)索引等。
本文將從Map接口的設(shè)計(jì)哲學(xué)出發(fā),深入剖析HashMap、LinkedHashMap、TreeMap、Hashtable、ConcurrentHashMap等主要實(shí)現(xiàn)類(lèi)的底層原理、源碼實(shí)現(xiàn)、性能特性,并結(jié)合Java 8+的新特性,幫助讀者全面掌握Map的使用技巧和選型策略。
第一章 Map接口概述
1.1 Map的繼承體系
Java中的Map體系是一個(gè)獨(dú)立于Collection的并行框架,其核心繼承結(jié)構(gòu)如下:
Map (interface)
├── HashMap (class)
│ └── LinkedHashMap (class)
├── TreeMap (class)
├── Hashtable (class)
│ └── Properties (class)
└── ConcurrentMap (interface)
└── ConcurrentHashMap (class)1.2 Map的核心特性
- 鍵唯一性:每個(gè)鍵最多映射到一個(gè)值,鍵的不可重復(fù)性通過(guò)
equals()和hashCode()保證 - 值可重復(fù):不同的鍵可以對(duì)應(yīng)相同的值
- 元素?zé)o序:大部分Map實(shí)現(xiàn)(如HashMap)不保證元素的順序
- 允許null:HashMap允許一個(gè)null鍵和多個(gè)null值,但Hashtable和ConcurrentHashMap不允許
1.3 存儲(chǔ)結(jié)構(gòu)的理解
從數(shù)據(jù)結(jié)構(gòu)角度看,Map的存儲(chǔ)可以分為三個(gè)層面:
- key視角:所有key構(gòu)成一個(gè)
Set集合 → 無(wú)序、不可重復(fù),key所在的類(lèi)必須重寫(xiě)equals()和hashCode() - value視角:所有value構(gòu)成一個(gè)
Collection集合 → 無(wú)序、可重復(fù),value所在的類(lèi)需要重寫(xiě)equals() - entry視角:每個(gè)key-value對(duì)構(gòu)成一個(gè)
Entry對(duì)象,所有entry構(gòu)成一個(gè)Set集合 → 無(wú)序、不可重復(fù)
這種設(shè)計(jì)體現(xiàn)了Map與Set、List的內(nèi)在聯(lián)系,也為后續(xù)的遍歷操作奠定了基礎(chǔ)。
第二章 HashMap:最常用的Map實(shí)現(xiàn)
HashMap是基于哈希表實(shí)現(xiàn)的Map,它根據(jù)鍵的hashCode值存儲(chǔ)數(shù)據(jù),具有O(1)的平均查找時(shí)間,是日常開(kāi)發(fā)中使用頻率最高的Map實(shí)現(xiàn)。
2.1 底層數(shù)據(jù)結(jié)構(gòu)演進(jìn)
HashMap的底層實(shí)現(xiàn)經(jīng)歷了從JDK 7到JDK 8的重要優(yōu)化:
| 版本 | 底層結(jié)構(gòu) | 節(jié)點(diǎn)類(lèi)型 | 特點(diǎn) |
|---|---|---|---|
| JDK 7 | 數(shù)組 + 鏈表 | Entry | 頭插法,擴(kuò)容時(shí)可能產(chǎn)生循環(huán)鏈表 |
| JDK 8+ | 數(shù)組 + 鏈表 + 紅黑樹(shù) | Node/TreeNode | 尾插法,鏈表長(zhǎng)度>8且數(shù)組長(zhǎng)度>64時(shí)樹(shù)化 |
2.2 核心源碼深度解析
2.2.1 重要成員變量
// 默認(rèn)初始容量16,必須是2的n次冪 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; // 默認(rèn)負(fù)載因子0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 鏈表轉(zhuǎn)紅黑樹(shù)閾值 static final int TREEIFY_THRESHOLD = 8; // 紅黑樹(shù)轉(zhuǎn)鏈表閾值 static final int UNTREEIFY_THRESHOLD = 6; // 樹(shù)化最小數(shù)組容量 static final int MIN_TREEIFY_CAPACITY = 64;
2.2.2 設(shè)計(jì)哲學(xué)解讀
為什么默認(rèn)負(fù)載因子是0.75?
負(fù)載因子表示散列表的空間使用程度。0.75是時(shí)間與空間的折中選擇:
- 過(guò)高(如1):空間利用率高,但Hash碰撞概率增加,鏈表變長(zhǎng),查詢(xún)效率下降
- 過(guò)低(如0.5):Hash碰撞減少,查詢(xún)快,但空間浪費(fèi)嚴(yán)重
為什么容量必須是2的n次冪?
這涉及HashMap的核心優(yōu)化:
- 高效取模:計(jì)算數(shù)組下標(biāo)時(shí),
(n - 1) & hash等價(jià)于hash % n,位運(yùn)算速度遠(yuǎn)快于取模 - 均勻分布:2^n-1的二進(jìn)制全是1,與運(yùn)算結(jié)果能充分利用hash值的所有位,減少碰撞
- 擴(kuò)容優(yōu)化:擴(kuò)容后元素的新位置要么在原位置,要么在原位置+舊容量,只需看hash值新增位是0還是1
為什么鏈表轉(zhuǎn)紅黑樹(shù)的閾值是8?
這是基于泊松分布的概率統(tǒng)計(jì)。在理想隨機(jī)hashCode下,鏈表節(jié)點(diǎn)數(shù)出現(xiàn)的概率遵循泊松分布,節(jié)點(diǎn)數(shù)為8的概率接近千萬(wàn)分之六,此時(shí)鏈表查詢(xún)性能已經(jīng)很差,轉(zhuǎn)為紅黑樹(shù)可以挽回性能。而樹(shù)節(jié)點(diǎn)占用的空間是普通節(jié)點(diǎn)的兩倍,當(dāng)節(jié)點(diǎn)數(shù)降到6時(shí)再轉(zhuǎn)回鏈表,避免頻繁轉(zhuǎn)換。
2.3 put方法執(zhí)行流程
HashMap的put方法是理解其工作原理的關(guān)鍵入口:
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ù)組延遲初始化:首次put時(shí)創(chuàng)建數(shù)組
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 計(jì)算下標(biāo),如果該位置為空直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 3. 處理Hash沖突
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 第一個(gè)節(jié)點(diǎn)就是要找的key
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 紅黑樹(shù)插入
else {
// 鏈表遍歷
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null); // 尾插法
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash); // 檢查是否需要樹(shù)化
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 4. 找到相同key,替換value
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 5. 檢查是否需要擴(kuò)容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}執(zhí)行流程總結(jié):
- 計(jì)算key的hash值(擾動(dòng)函數(shù):高16位與低16位異或)
- 通過(guò)
(n - 1) & hash計(jì)算數(shù)組下標(biāo) - 如果該位置為空,直接插入
- 如果該位置不為空,遍歷鏈表或紅黑樹(shù)
- 找到相同key則替換value,否則插入新節(jié)點(diǎn)
- 檢查是否需要樹(shù)化或擴(kuò)容
2.4 擴(kuò)容機(jī)制(resize)
當(dāng)元素個(gè)數(shù)超過(guò)threshold = capacity * loadFactor時(shí),HashMap會(huì)進(jìn)行擴(kuò)容:
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 計(jì)算新容量
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 容量翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 閾值也翻倍
}
// ... 初始化邏輯
// 創(chuàng)建新數(shù)組
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 數(shù)據(jù)遷移
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
// 單個(gè)節(jié)點(diǎn)直接重新計(jì)算下標(biāo)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 紅黑樹(shù)拆分
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// 鏈表拆分:保持原順序
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 關(guān)鍵優(yōu)化:根據(jù)hash值新增位判斷新位置
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;
}
e = next;
} while (e != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead; // 原索引位置
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead; // 原索引+舊容量
}
}
}
}
}
return newTab;
}擴(kuò)容優(yōu)化點(diǎn):
- JDK 8采用尾插法,避免JDK 7頭插法在多線(xiàn)程環(huán)境下產(chǎn)生的循環(huán)鏈表問(wèn)題
- 元素遷移時(shí),無(wú)需重新計(jì)算hash,只需看
e.hash & oldCap是否為0,為0則留在原位,否則移到原位置+oldCap - 鏈表保持原順序,不會(huì)倒置
2.5 線(xiàn)程安全問(wèn)題
HashMap是線(xiàn)程不安全的,多線(xiàn)程環(huán)境下可能出現(xiàn)以下問(wèn)題:
- 數(shù)據(jù)覆蓋:兩個(gè)線(xiàn)程同時(shí)put,計(jì)算出的下標(biāo)相同,一個(gè)線(xiàn)程插入的數(shù)據(jù)可能被另一個(gè)覆蓋
- size不準(zhǔn)確:
++size操作非原子性,多個(gè)線(xiàn)程同時(shí)put可能導(dǎo)致size偏小 - JDK 7擴(kuò)容死循環(huán):頭插法在并發(fā)擴(kuò)容時(shí)可能形成環(huán)形鏈表,導(dǎo)致CPU 100%
解決方案:
- 使用
Collections.synchronizedMap(new HashMap<>()) - 使用
ConcurrentHashMap(推薦)
第三章 LinkedHashMap:保持插入順序
LinkedHashMap繼承自HashMap,在HashMap基礎(chǔ)上通過(guò)雙向鏈表維護(hù)元素的順序。
3.1 數(shù)據(jù)結(jié)構(gòu)特點(diǎn)
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // 前驅(qū)和后繼指針
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
LinkedHashMap在HashMap的Node基礎(chǔ)上增加了before和after指針,構(gòu)成了一個(gè)雙向鏈表,用于記錄元素的插入順序或訪(fǎng)問(wèn)順序。
3.2 兩種排序模式
LinkedHashMap支持兩種迭代順序:
- 插入順序(默認(rèn)):按元素首次插入Map的順序迭代
- 訪(fǎng)問(wèn)順序:按元素最近被訪(fǎng)問(wèn)(get/put)的時(shí)間從舊到新迭代
// 指定訪(fǎng)問(wèn)順序
Map<String, String> map = new LinkedHashMap<>(16, 0.75f, true);
map.put("a", "1");
map.put("b", "2");
map.get("a"); // 訪(fǎng)問(wèn)a,a會(huì)被移動(dòng)到鏈表尾部
// 迭代順序:b, a(最近訪(fǎng)問(wèn)的在最后)
3.3 實(shí)現(xiàn)LRU緩存
利用訪(fǎng)問(wèn)順序模式,可以輕松實(shí)現(xiàn)LRU(Least Recently Used)緩存:
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxCapacity;
public LRUCache(int maxCapacity) {
super(16, 0.75f, true); // 啟用訪(fǎng)問(wèn)順序
this.maxCapacity = maxCapacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxCapacity; // 超過(guò)容量時(shí)移除最久未訪(fǎng)問(wèn)的元素
}
}3.4 性能特點(diǎn)
- 遍歷速度:只與元素個(gè)數(shù)有關(guān),與HashMap容量無(wú)關(guān),因此當(dāng)HashMap容量大而實(shí)際元素少時(shí),LinkedHashMap遍歷更快
- 插入性能:略低于HashMap,因?yàn)樾枰S護(hù)雙向鏈表
- 內(nèi)存占用:比HashMap多兩個(gè)指針的開(kāi)銷(xiāo)
第四章 TreeMap:基于紅黑樹(shù)的排序Map
TreeMap實(shí)現(xiàn)了SortedMap和NavigableMap接口,底層基于紅黑樹(shù)實(shí)現(xiàn),能夠?qū)︽I進(jìn)行排序。
4.1 排序機(jī)制
TreeMap要求鍵要么實(shí)現(xiàn)Comparable接口(自然排序),要么在構(gòu)造時(shí)提供Comparator(定制排序):
// 自然排序:鍵必須實(shí)現(xiàn)Comparable
TreeMap<Integer, String> naturalMap = new TreeMap<>();
// 定制排序:提供Comparator
TreeMap<String, Integer> customMap = new TreeMap<>(
(s1, s2) -> s2.compareTo(s1) // 降序
);4.2 核心方法
TreeMap提供了豐富的導(dǎo)航方法:
TreeMap<Integer, String> map = new TreeMap<>(); map.put(1, "one"); map.put(3, "three"); map.put(5, "five"); map.put(7, "seven"); Integer firstKey = map.firstKey(); // 1 Integer lastKey = map.lastKey(); // 7 Integer lowerKey = map.lowerKey(5); // 3(小于5的最大鍵) Integer floorKey = map.floorKey(4); // 3(小于等于4的最大鍵) Integer ceilingKey = map.ceilingKey(4); // 5(大于等于4的最小鍵) Integer higherKey = map.higherKey(5); // 7(大于5的最小鍵) // 子Map視圖 SortedMap<Integer, String> headMap = map.headMap(5); // 鍵<5的部分 SortedMap<Integer, String> tailMap = map.tailMap(5); // 鍵>=5的部分 SortedMap<Integer, String> subMap = map.subMap(3, 6); // 3<=鍵<6
4.3 源碼分析:compare方法
TreeMap的核心是比較邏輯,它在put、get、remove等操作中都會(huì)用到:
final int compare(Object k1, Object k2) {
return comparator == null ?
((Comparable<? super K>)k1).compareTo((K)k2) :
comparator.compare((K)k1, (K)k2);
}
如果既沒(méi)有提供Comparator,鍵也沒(méi)有實(shí)現(xiàn)Comparable,在插入時(shí)會(huì)拋出ClassCastException。
4.4 注意事項(xiàng)
- 鍵不能為null:因?yàn)闊o(wú)法比較null
- compareTo與equals需一致:當(dāng)兩個(gè)鍵比較結(jié)果為0時(shí),TreeMap認(rèn)為它們相等,即使
equals返回false - 字符串鍵的特殊性:字符串的
compareTo基于Unicode值,數(shù)字字符串排序時(shí)需注意// 錯(cuò)誤:字符串排序按字典序,"22"會(huì)排在"5"前面 TreeMap<String, Integer> map = new TreeMap<>(); map.put("5", 1); map.put("22", 2); // 實(shí)際順序:22, 5 // 正確:轉(zhuǎn)為整數(shù)比較 TreeMap<String, Integer> map = new TreeMap<>( (a, b) -> Integer.parseInt(a) - Integer.parseInt(b) );
第五章 Hashtable與Properties
5.1 Hashtable:古老的線(xiàn)程安全Map
Hashtable是JDK 1.0就存在的古老實(shí)現(xiàn)類(lèi),具有以下特點(diǎn):
- 線(xiàn)程安全:所有方法都用
synchronized修飾 - 不允許null鍵和null值:否則拋出NullPointerException
- 初始容量11,擴(kuò)容為
2*old+1 - 性能較低:全表鎖導(dǎo)致并發(fā)性能差
Hashtable<String, Integer> table = new Hashtable<>();
table.put("key", 1);
// table.put(null, 2); // 運(yùn)行時(shí)異常
性能對(duì)比:
- 寫(xiě)入速度:Hashtable可能比HashMap快(測(cè)試數(shù)據(jù):1420ms vs 797ms)
- 讀取速度:HashMap比Hashtable快(188ms vs 265ms)
5.2 Properties:處理配置文件
Properties繼承自Hashtable,專(zhuān)門(mén)用于處理配置文件,鍵和值都是String類(lèi)型。
Properties props = new Properties();
props.setProperty("url", "jdbc:mysql://localhost:3306/db");
props.setProperty("username", "root");
props.setProperty("password", "123456");
// 加載配置文件
try (InputStream input = new FileInputStream("config.properties")) {
props.load(input);
String url = props.getProperty("url");
String username = props.getProperty("username");
}常用方法:
load(InputStream)/store(OutputStream):加載/存儲(chǔ)配置文件getProperty(String key, String defaultValue):獲取屬性,可指定默認(rèn)值list(PrintStream):打印所有屬性
第六章 ConcurrentHashMap:并發(fā)編程的利器
ConcurrentHashMap是Java并發(fā)包(java.util.concurrent)中提供的線(xiàn)程安全且高性能的Map實(shí)現(xiàn)。
6.1 設(shè)計(jì)哲學(xué)
ConcurrentHashMap的設(shè)計(jì)目標(biāo)是:在保證線(xiàn)程安全的同時(shí),提供比Hashtable更高的并發(fā)性能。
| 實(shí)現(xiàn)類(lèi) | 鎖策略 | 并發(fā)度 | 性能 |
|---|---|---|---|
| Hashtable | 全表鎖 | 極低 | 差 |
| Collections.synchronizedMap | 全表鎖 | 極低 | 差 |
| ConcurrentHashMap JDK 7 | 分段鎖 | 16 | 高 |
| ConcurrentHashMap JDK 8+ | CAS + synchronized + 細(xì)粒度鎖 | 極高 | 非常高 |
6.2 JDK 7實(shí)現(xiàn):分段鎖
JDK 7的ConcurrentHashMap采用Segment分段鎖機(jī)制:
- 將整個(gè)Map分成多個(gè)Segment(默認(rèn)16個(gè))
- 每個(gè)Segment獨(dú)立加鎖,相當(dāng)于一個(gè)小型的HashMap
- 不同Segment的寫(xiě)操作可以并發(fā)執(zhí)行
- 讀操作幾乎不加鎖(volatile保證可見(jiàn)性)
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K,V>[] table;
// ...
}
6.3 JDK 8+實(shí)現(xiàn):CAS + synchronized
JDK 8對(duì)ConcurrentHashMap進(jìn)行了重大重構(gòu):
- 放棄分段鎖,改用CAS + synchronized實(shí)現(xiàn)
- 與HashMap結(jié)構(gòu)對(duì)齊:數(shù)組+鏈表+紅黑樹(shù)
- 鎖粒度更細(xì):只鎖住鏈表或紅黑樹(shù)的頭節(jié)點(diǎn)
- 讀操作完全無(wú)鎖(volatile保證可見(jiàn)性)
// putVal核心片段
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ... 非空校驗(yàn)等
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); // 初始化,CAS保證線(xiàn)程安全
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 該位置為空,CAS嘗試插入
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f); // 幫助擴(kuò)容
else {
V oldVal = null;
synchronized (f) { // 鎖住鏈表頭節(jié)點(diǎn)
// 鏈表或紅黑樹(shù)操作
}
}
}
}6.4 弱一致性迭代器
ConcurrentHashMap的迭代器是弱一致性的:
- 迭代器創(chuàng)建后,如果Map發(fā)生修改,不會(huì)拋出
ConcurrentModificationException - 迭代器反映的是創(chuàng)建時(shí)刻或之后某個(gè)時(shí)刻的數(shù)據(jù)快照
- 迭代過(guò)程中修改Map,迭代器可能看到,也可能看不到修改結(jié)果
- 適用于高并發(fā)場(chǎng)景,避免了快速失敗機(jī)制帶來(lái)的問(wèn)題
6.5 批量操作
ConcurrentHashMap提供了強(qiáng)大的批量操作API:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// forEach:遍歷每個(gè)元素
map.forEach(1, (k, v) -> System.out.println(k + ":" + v));
// search:查找第一個(gè)符合條件的元素
String result = map.search(1, (k, v) -> v > 100 ? k : null);
// reduce:累加操作
Integer sum = map.reduceValues(1, Integer::sum);
// 用作頻率統(tǒng)計(jì)(MultiSet)
ConcurrentHashMap<String, LongAdder> freqs = new ConcurrentHashMap<>();
freqs.computeIfAbsent("word", k -> new LongAdder()).increment();parallelismThreshold參數(shù)控制并行度:小于閾值時(shí)串行執(zhí)行,大于閾值時(shí)并行執(zhí)行。
第七章 Map常用方法詳解
7.1 基礎(chǔ)操作方法
| 方法 | 描述 | 返回值說(shuō)明 |
|---|---|---|
put(K key, V value) | 添加鍵值對(duì) | 返回該key之前的value,如果沒(méi)有則返回null |
get(Object key) | 根據(jù)key獲取value | 存在則返回value,否則返回null |
remove(Object key) | 刪除鍵值對(duì) | 返回被刪除的value |
clear() | 清空所有鍵值對(duì) | void |
size() | 返回鍵值對(duì)數(shù)量 | int |
isEmpty() | 判斷是否為空 | boolean |
7.2 查詢(xún)方法
| 方法 | 描述 |
|---|---|
containsKey(Object key) | 判斷是否包含指定鍵 |
containsValue(Object value) | 判斷是否包含指定值(HashMap中效率較低,需遍歷) |
getOrDefault(Object key, V defaultValue) | 獲取值,不存在則返回默認(rèn)值 |
7.3 遍歷方法
Map的遍歷方式多樣,可根據(jù)場(chǎng)景選擇:
7.3.1 entrySet遍歷(最常用)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
7.3.2 keySet + get遍歷
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
// 缺點(diǎn):每次get都需要二次查找,效率較低
7.3.3 values遍歷(僅需值時(shí))
for (Integer value : map.values()) {
System.out.println(value);
}
7.3.4 Iterator遍歷(支持remove)
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
if (entry.getValue() < 0) {
iterator.remove(); // 安全刪除
}
}
7.3.5 Java 8 forEach(最簡(jiǎn)潔)
map.forEach((key, value) -> System.out.println(key + ": " + value));
7.3.6 Stream API遍歷(支持鏈?zhǔn)讲僮鳎?/h4>
map.entrySet().stream()
.filter(entry -> entry.getValue() > 10)
.forEach(entry -> System.out.println(entry.getKey()));
map.entrySet().stream()
.filter(entry -> entry.getValue() > 10)
.forEach(entry -> System.out.println(entry.getKey()));
7.4 Java 8+新增的默認(rèn)方法
Java 8在Map接口中增加了多個(gè)實(shí)用默認(rèn)方法,極大地簡(jiǎn)化了代碼:
7.4.1 computeIfAbsent / computeIfPresent
// 如果key不存在,則通過(guò)函數(shù)計(jì)算value并放入Map
map.computeIfAbsent("key", k -> new ArrayList<>()).add("value");
// 經(jīng)典用法:實(shí)現(xiàn)多值Map
Map<String, List<String>> multiMap = new HashMap<>();
multiMap.computeIfAbsent("group1", k -> new ArrayList<>()).add("item1");
// 如果key存在,則根據(jù)原值計(jì)算新值
map.computeIfPresent("key", (k, v) -> v * 2);7.4.2 merge方法
// 合并操作:如果key不存在則放入給定值,存在則通過(guò)合并函數(shù)計(jì)算新值
map.merge("key", 1, Integer::sum); // 統(tǒng)計(jì)功能
// 經(jīng)典用法:?jiǎn)卧~計(jì)數(shù)
String text = "apple banana apple orange apple";
Map<String, Integer> wordCount = new HashMap<>();
for (String word : text.split(" ")) {
wordCount.merge(word, 1, Integer::sum);
}
// 結(jié)果:{apple=3, banana=1, orange=1}7.4.3 putIfAbsent
// 僅在key不存在時(shí)放入
map.putIfAbsent("key", "value");
7.4.4 replace / replaceAll
// 替換指定key的值(僅當(dāng)存在時(shí))
map.replace("key", "newValue");
// 對(duì)所有entry應(yīng)用替換函數(shù)
map.replaceAll((k, v) -> v.toUpperCase());7.5 Java 9的Map.of工廠(chǎng)方法
Java 9提供了更簡(jiǎn)潔的Map初始化方式:
// 創(chuàng)建不可變Map(最多支持10對(duì)鍵值)
Map<String, Integer> map1 = Map.of(
"a", 1,
"b", 2,
"c", 3
);
// 任意數(shù)量鍵值對(duì)
Map<String, Integer> map2 = Map.ofEntries(
Map.entry("a", 1),
Map.entry("b", 2),
Map.entry("c", 3),
Map.entry("d", 4)
);第八章 實(shí)現(xiàn)類(lèi)對(duì)比與選型指南
8.1 核心特性對(duì)比
| 特性 | HashMap | LinkedHashMap | TreeMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|---|---|
| 順序 | 無(wú)序 | 插入/訪(fǎng)問(wèn)順序 | 鍵排序 | 無(wú)序 | 無(wú)序 |
| null鍵 | 允許1個(gè) | 允許1個(gè) | 不允許 | 不允許 | 不允許 |
| null值 | 允許 | 允許 | 允許 | 不允許 | 不允許 |
| 線(xiàn)程安全 | 否 | 否 | 否 | 是(全表鎖) | 是(分段/CAS) |
| 性能 | 最高 | 略低于HashMap | 較低(log n) | 讀慢寫(xiě)快 | 高并發(fā)下最優(yōu) |
| 底層結(jié)構(gòu) | 數(shù)組+鏈表+紅黑樹(shù) | 數(shù)組+鏈表+紅黑樹(shù)+雙向鏈表 | 紅黑樹(shù) | 數(shù)組+鏈表 | CAS+數(shù)組+鏈表+紅黑樹(shù) |
| 適用場(chǎng)景 | 通用緩存 | 需保持順序 | 需排序/范圍查詢(xún) | 遺留系統(tǒng) | 高并發(fā)共享數(shù)據(jù) |
8.2 時(shí)間復(fù)雜度對(duì)比
| 操作 | HashMap | LinkedHashMap | TreeMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|---|---|
| get | O(1) | O(1) | O(log n) | O(1) | O(1) |
| put | O(1) | O(1) | O(log n) | O(1) | O(1) |
| remove | O(1) | O(1) | O(log n) | O(1) | O(1) |
| containsKey | O(1) | O(1) | O(log n) | O(1) | O(1) |
| containsValue | O(n) | O(n) | O(n) | O(n) | O(n) |
8.3 選型建議
根據(jù)不同的業(yè)務(wù)場(chǎng)景,選擇合適的Map實(shí)現(xiàn):
場(chǎng)景1:通用緩存,無(wú)特殊順序要求
- ? 首選:HashMap(性能最高)
- 如果線(xiàn)程安全要求:ConcurrentHashMap
場(chǎng)景2:需要保持插入順序
- ? 首選:LinkedHashMap
- 案例:實(shí)現(xiàn)FIFO隊(duì)列、記錄操作日志
場(chǎng)景3:需要按鍵排序或范圍查詢(xún)
- ? 首選:TreeMap
- 案例:排行榜、日程表、字典序輸出
場(chǎng)景4:實(shí)現(xiàn)LRU緩存
- ? 首選:LinkedHashMap(訪(fǎng)問(wèn)順序模式)
- 案例:內(nèi)存緩存、最近訪(fǎng)問(wèn)記錄
場(chǎng)景5:高并發(fā)共享數(shù)據(jù)
- ? 首選:ConcurrentHashMap
- 案例:全局配置、在線(xiàn)用戶(hù)統(tǒng)計(jì)
場(chǎng)景6:處理配置文件
- ? 首選:Properties
- 案例:讀取application.properties
8.4 性能測(cè)試數(shù)據(jù)參考
根據(jù)實(shí)際測(cè)試(百萬(wàn)級(jí)數(shù)據(jù)):
| 操作 | HashMap | LinkedHashMap | TreeMap | Hashtable |
|---|---|---|---|---|
| 插入100萬(wàn)條 | 1420ms | 1512ms | 3845ms | 797ms |
| 讀取1000萬(wàn)條 | 188ms | 201ms | 892ms | 265ms |
注:Hashtable插入快可能是由于其初始容量較小,擴(kuò)容頻率高導(dǎo)致的測(cè)試偏差,實(shí)際應(yīng)用中HashMap綜合性能最優(yōu)。
第九章 常見(jiàn)陷阱與最佳實(shí)踐
9.1 陷阱一:可變對(duì)象作為鍵
// 錯(cuò)誤示例
Map<List<String>, String> map = new HashMap<>();
List<String> key = new ArrayList<>();
key.add("a");
map.put(key, "value1");
key.add("b"); // 鍵被修改,hashCode改變
map.get(key); // 返回null,再也找不到
map.containsKey(key); // false解決方案:使用不可變對(duì)象作為鍵,如String、Integer,或自定義不可變類(lèi)。
9.2 陷阱二:自定義類(lèi)未重寫(xiě)hashCode和equals
class User {
String name;
// 沒(méi)有重寫(xiě)hashCode和equals
}
Map<User, Integer> map = new HashMap<>();
User u1 = new User("Alice");
User u2 = new User("Alice");
map.put(u1, 100);
map.get(u2); // 返回null,雖然內(nèi)容相同解決方案:作為鍵的類(lèi)必須正確重寫(xiě)hashCode()和equals()。
9.3 陷阱三:并發(fā)修改導(dǎo)致ConcurrentModificationException
Map<String, Integer> map = new HashMap<>();
// ... 填充數(shù)據(jù)
for (String key : map.keySet()) {
if (key.startsWith("temp")) {
map.remove(key); // 拋出ConcurrentModificationException
}
}
解決方案:
// 方式1:使用Iterator的remove
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
if (key.startsWith("temp")) {
it.remove();
}
}
// 方式2:使用removeIf(Java 8+)
map.keySet().removeIf(key -> key.startsWith("temp"));
// 方式3:使用ConcurrentHashMap(允許并發(fā)修改)9.4 最佳實(shí)踐總結(jié)
- 預(yù)估初始容量:如果能預(yù)知數(shù)據(jù)規(guī)模,指定初始容量避免頻繁擴(kuò)容
Map<String, Integer> map = new HashMap<>(expectedSize * 4 / 3 + 1);
- 使用泛型:指定鍵值類(lèi)型,避免運(yùn)行時(shí)類(lèi)型轉(zhuǎn)換異常
- 優(yōu)先使用Java 8+默認(rèn)方法:讓代碼更簡(jiǎn)潔
// 老式
if (!map.containsKey(key)) {
map.put(key, new ArrayList<>());
}
map.get(key).add(value);
// 新式
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);- 選擇合適的實(shí)現(xiàn):根據(jù)業(yè)務(wù)需求而非習(xí)慣選擇
- 注意線(xiàn)程安全:多線(xiàn)程環(huán)境優(yōu)先使用ConcurrentHashMap
- 避免使用Hashtable:除非維護(hù)遺留代碼
結(jié)語(yǔ)
Java Map體系經(jīng)過(guò)多年的演進(jìn),從最早的Hashtable,到JDK 1.2引入的HashMap,再到JDK 1.5的ConcurrentHashMap,以及后續(xù)的各種優(yōu)化,已經(jīng)形成了一套功能完備、性能卓越的數(shù)據(jù)結(jié)構(gòu)家族。
理解Map的核心原理,不僅有助于寫(xiě)出更高效的代碼,還能在遇到復(fù)雜業(yè)務(wù)場(chǎng)景時(shí)做出正確的技術(shù)選型。本文從源碼層面剖析了各個(gè)Map實(shí)現(xiàn)類(lèi)的底層機(jī)制,并結(jié)合實(shí)際場(chǎng)景給出了使用建議。在實(shí)際開(kāi)發(fā)中,建議遵循"面向接口編程"的原則,根據(jù)具體需求選擇最合適的Map實(shí)現(xiàn),同時(shí)注意線(xiàn)程安全和鍵的不可變性等關(guān)鍵問(wèn)題。
Map的學(xué)習(xí)是一個(gè)循序漸進(jìn)的過(guò)程,掌握基礎(chǔ)用法后,深入理解其設(shè)計(jì)思想和源碼實(shí)現(xiàn),才能真正做到"知其然,知其所以然"。
到此這篇關(guān)于Java Map常用方法和實(shí)現(xiàn)類(lèi)深度詳解的文章就介紹到這了,更多相關(guān)java map常用方法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
使用Spring Security控制會(huì)話(huà)的方法
在本文中,我們將說(shuō)明Spring Security如何允許我們控制HTTP會(huì)話(huà)。這篇文章主要介紹了使用Spring Security控制會(huì)話(huà) ,需要的朋友可以參考下2019-05-05
SpringBoot入門(mén)實(shí)現(xiàn)第一個(gè)SpringBoot項(xiàng)目
今天我們一起來(lái)完成一個(gè)簡(jiǎn)單的SpringBoot(Hello World)。就把他作為你的第一個(gè)SpringBoot項(xiàng)目。具有一定的參考價(jià)值,感興趣的可以了解一下2021-09-09
Spring MultipartFile實(shí)現(xiàn)多文件上傳攻略
這篇文章主要介紹了Spring MultipartFile實(shí)現(xiàn)多文件上傳,MultipartFile是Spring框架中用于處理文件上傳的核心接口,MultipartFile的使用需注意文件驗(yàn)證和錯(cuò)誤處理,以保證系統(tǒng)的穩(wěn)定性和安全性,需要的朋友可以參考下2025-10-10
Spring+MyBatis實(shí)現(xiàn)數(shù)據(jù)讀寫(xiě)分離的實(shí)例代碼
本篇文章主要介紹了Spring+MyBatis實(shí)現(xiàn)數(shù)據(jù)讀寫(xiě)分離的實(shí)例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07

