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


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

