
文章目录
-
- 前言
- [第一章 Map接口概述](#第一章 Map接口概述)
-
- [1.1 Map的继承体系](#1.1 Map的继承体系)
- [1.2 Map的核心特性](#1.2 Map的核心特性)
- [1.3 存储结构的理解](#1.3 存储结构的理解)
- [第二章 HashMap:最常用的Map实现](#第二章 HashMap:最常用的Map实现)
-
- [2.1 底层数据结构演进](#2.1 底层数据结构演进)
- [2.2 核心源码深度解析](#2.2 核心源码深度解析)
-
- [2.2.1 重要成员变量](#2.2.1 重要成员变量)
- [2.2.2 设计哲学解读](#2.2.2 设计哲学解读)
- [2.3 put方法执行流程](#2.3 put方法执行流程)
- [2.4 扩容机制(resize)](#2.4 扩容机制(resize))
- [2.5 线程安全问题](#2.5 线程安全问题)
- [第三章 LinkedHashMap:保持插入顺序](#第三章 LinkedHashMap:保持插入顺序)
-
- [3.1 数据结构特点](#3.1 数据结构特点)
- [3.2 两种排序模式](#3.2 两种排序模式)
- [3.3 实现LRU缓存](#3.3 实现LRU缓存)
- [3.4 性能特点](#3.4 性能特点)
- [第四章 TreeMap:基于红黑树的排序Map](#第四章 TreeMap:基于红黑树的排序Map)
-
- [4.1 排序机制](#4.1 排序机制)
- [4.2 核心方法](#4.2 核心方法)
- [4.3 源码分析:compare方法](#4.3 源码分析:compare方法)
- [4.4 注意事项](#4.4 注意事项)
- [第五章 Hashtable与Properties](#第五章 Hashtable与Properties)
-
- [5.1 Hashtable:古老的线程安全Map](#5.1 Hashtable:古老的线程安全Map)
- [5.2 Properties:处理配置文件](#5.2 Properties:处理配置文件)
- [第六章 ConcurrentHashMap:并发编程的利器](#第六章 ConcurrentHashMap:并发编程的利器)
-
- [6.1 设计哲学](#6.1 设计哲学)
- [6.2 JDK 7实现:分段锁](#6.2 JDK 7实现:分段锁)
- [6.3 JDK 8+实现:CAS + synchronized](#6.3 JDK 8+实现:CAS + synchronized)
- [6.4 弱一致性迭代器](#6.4 弱一致性迭代器)
- [6.5 批量操作](#6.5 批量操作)
- [第七章 Map常用方法详解](#第七章 Map常用方法详解)
-
- [7.1 基础操作方法](#7.1 基础操作方法)
- [7.2 查询方法](#7.2 查询方法)
- [7.3 遍历方法](#7.3 遍历方法)
-
- [7.3.1 entrySet遍历(最常用)](#7.3.1 entrySet遍历(最常用))
- [7.3.2 keySet + get遍历](#7.3.2 keySet + get遍历)
- [7.3.3 values遍历(仅需值时)](#7.3.3 values遍历(仅需值时))
- [7.3.4 Iterator遍历(支持remove)](#7.3.4 Iterator遍历(支持remove))
- [7.3.5 Java 8 forEach(最简洁)](#7.3.5 Java 8 forEach(最简洁))
- [7.3.6 Stream API遍历(支持链式操作)](#7.3.6 Stream API遍历(支持链式操作))
- [7.4 Java 8+新增的默认方法](#7.4 Java 8+新增的默认方法)
-
- [7.4.1 computeIfAbsent / computeIfPresent](#7.4.1 computeIfAbsent / computeIfPresent)
- [7.4.2 merge方法](#7.4.2 merge方法)
- [7.4.3 putIfAbsent](#7.4.3 putIfAbsent)
- [7.4.4 replace / replaceAll](#7.4.4 replace / replaceAll)
- [7.5 Java 9的Map.of工厂方法](#7.5 Java 9的Map.of工厂方法)
- [第八章 实现类对比与选型指南](#第八章 实现类对比与选型指南)
-
- [8.1 核心特性对比](#8.1 核心特性对比)
- [8.2 时间复杂度对比](#8.2 时间复杂度对比)
- [8.3 选型建议](#8.3 选型建议)
- [8.4 性能测试数据参考](#8.4 性能测试数据参考)
- [第九章 常见陷阱与最佳实践](#第九章 常见陷阱与最佳实践)
-
- [9.1 陷阱一:可变对象作为键](#9.1 陷阱一:可变对象作为键)
- [9.2 陷阱二:自定义类未重写hashCode和equals](#9.2 陷阱二:自定义类未重写hashCode和equals)
- [9.3 陷阱三:并发修改导致ConcurrentModificationException](#9.3 陷阱三:并发修改导致ConcurrentModificationException)
- [9.4 最佳实践总结](#9.4 最佳实践总结)
- 结语

前言
在Java集合框架中,Map是最核心、最常用的数据结构之一。与Collection体系下的List、Set不同,Map采用**键值对(Key-Value)**的存储方式,每个键映射到一个值,键在同一个Map中不可重复。这种设计使得Map特别适合需要通过键快速查找值的场景,如缓存系统、配置管理、数据索引等。
本文将从Map接口的设计哲学出发,深入剖析HashMap、LinkedHashMap、TreeMap、Hashtable、ConcurrentHashMap等主要实现类的底层原理、源码实现、性能特性,并结合Java 8+的新特性,帮助读者全面掌握Map的使用技巧和选型策略。
第一章 Map接口概述
1.1 Map的继承体系
Java中的Map体系是一个独立于Collection的并行框架,其核心继承结构如下:
Map (interface)
├── HashMap (class)
│ └── LinkedHashMap (class)
├── TreeMap (class)
├── Hashtable (class)
│ └── Properties (class)
└── ConcurrentMap (interface)
└── ConcurrentHashMap (class)
1.2 Map的核心特性
- 键唯一性 :每个键最多映射到一个值,键的不可重复性通过
equals()和hashCode()保证 - 值可重复:不同的键可以对应相同的值
- 元素无序:大部分Map实现(如HashMap)不保证元素的顺序
- 允许null:HashMap允许一个null键和多个null值,但Hashtable和ConcurrentHashMap不允许
1.3 存储结构的理解
从数据结构角度看,Map的存储可以分为三个层面:
- key视角 :所有key构成一个
Set集合 → 无序、不可重复,key所在的类必须重写equals()和hashCode() - value视角 :所有value构成一个
Collection集合 → 无序、可重复,value所在的类需要重写equals() - entry视角 :每个key-value对构成一个
Entry对象,所有entry构成一个Set集合 → 无序、不可重复
这种设计体现了Map与Set、List的内在联系,也为后续的遍历操作奠定了基础。
第二章 HashMap:最常用的Map实现
HashMap是基于哈希表 实现的Map,它根据键的hashCode值存储数据,具有O(1)的平均查找时间,是日常开发中使用频率最高的Map实现。
2.1 底层数据结构演进
HashMap的底层实现经历了从JDK 7到JDK 8的重要优化:
| 版本 | 底层结构 | 节点类型 | 特点 |
|---|---|---|---|
| JDK 7 | 数组 + 链表 | Entry | 头插法,扩容时可能产生循环链表 |
| JDK 8+ | 数组 + 链表 + 红黑树 | Node/TreeNode | 尾插法,链表长度>8且数组长度>64时树化 |
2.2 核心源码深度解析
2.2.1 重要成员变量
java
// 默认初始容量16,必须是2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 链表转红黑树阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树转链表阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 树化最小数组容量
static final int MIN_TREEIFY_CAPACITY = 64;
2.2.2 设计哲学解读
为什么默认负载因子是0.75?
负载因子表示散列表的空间使用程度。0.75是时间与空间的折中选择:
- 过高(如1):空间利用率高,但Hash碰撞概率增加,链表变长,查询效率下降
- 过低(如0.5):Hash碰撞减少,查询快,但空间浪费严重
为什么容量必须是2的n次幂?
这涉及HashMap的核心优化:
- 高效取模 :计算数组下标时,
(n - 1) & hash等价于hash % n,位运算速度远快于取模 - 均匀分布:2^n-1的二进制全是1,与运算结果能充分利用hash值的所有位,减少碰撞
- 扩容优化:扩容后元素的新位置要么在原位置,要么在原位置+旧容量,只需看hash值新增位是0还是1
为什么链表转红黑树的阈值是8?
这是基于泊松分布 的概率统计。在理想随机hashCode下,链表节点数出现的概率遵循泊松分布,节点数为8的概率接近千万分之六,此时链表查询性能已经很差,转为红黑树可以挽回性能。而树节点占用的空间是普通节点的两倍,当节点数降到6时再转回链表,避免频繁转换。
2.3 put方法执行流程
HashMap的put方法是理解其工作原理的关键入口:
java
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. 数组延迟初始化:首次put时创建数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 计算下标,如果该位置为空直接插入
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; // 第一个节点就是要找的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. 检查是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
执行流程总结:
- 计算key的hash值(扰动函数:高16位与低16位异或)
- 通过
(n - 1) & hash计算数组下标 - 如果该位置为空,直接插入
- 如果该位置不为空,遍历链表或红黑树
- 找到相同key则替换value,否则插入新节点
- 检查是否需要树化或扩容
2.4 扩容机制(resize)
当元素个数超过threshold = capacity * loadFactor时,HashMap会进行扩容:
java
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; // 阈值也翻倍
}
// ... 初始化逻辑
// 创建新数组
@SuppressWarnings({"rawtypes","unchecked"})
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;
if (e.next == null)
// 单个节点直接重新计算下标
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;
// 关键优化:根据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;
}
扩容优化点:
- JDK 8采用尾插法,避免JDK 7头插法在多线程环境下产生的循环链表问题
- 元素迁移时,无需重新计算hash,只需看
e.hash & oldCap是否为0,为0则留在原位,否则移到原位置+oldCap - 链表保持原顺序,不会倒置
2.5 线程安全问题
HashMap是线程不安全的,多线程环境下可能出现以下问题:
- 数据覆盖:两个线程同时put,计算出的下标相同,一个线程插入的数据可能被另一个覆盖
- size不准确 :
++size操作非原子性,多个线程同时put可能导致size偏小 - JDK 7扩容死循环:头插法在并发扩容时可能形成环形链表,导致CPU 100%
解决方案:
- 使用
Collections.synchronizedMap(new HashMap<>()) - 使用
ConcurrentHashMap(推荐)
第三章 LinkedHashMap:保持插入顺序
LinkedHashMap继承自HashMap,在HashMap基础上通过双向链表维护元素的顺序。
3.1 数据结构特点
java
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // 前驱和后继指针
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
LinkedHashMap在HashMap的Node基础上增加了before和after指针,构成了一个双向链表,用于记录元素的插入顺序或访问顺序。
3.2 两种排序模式
LinkedHashMap支持两种迭代顺序:
- 插入顺序(默认):按元素首次插入Map的顺序迭代
- 访问顺序:按元素最近被访问(get/put)的时间从旧到新迭代
java
// 指定访问顺序
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 实现LRU缓存
利用访问顺序模式,可以轻松实现LRU(Least Recently Used)缓存:
java
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 性能特点
- 遍历速度:只与元素个数有关,与HashMap容量无关,因此当HashMap容量大而实际元素少时,LinkedHashMap遍历更快
- 插入性能:略低于HashMap,因为需要维护双向链表
- 内存占用:比HashMap多两个指针的开销
第四章 TreeMap:基于红黑树的排序Map
TreeMap实现了SortedMap和NavigableMap接口,底层基于红黑树实现,能够对键进行排序。
4.1 排序机制
TreeMap要求键要么实现Comparable接口(自然排序),要么在构造时提供Comparator(定制排序):
java
// 自然排序:键必须实现Comparable
TreeMap<Integer, String> naturalMap = new TreeMap<>();
// 定制排序:提供Comparator
TreeMap<String, Integer> customMap = new TreeMap<>(
(s1, s2) -> s2.compareTo(s1) // 降序
);
4.2 核心方法
TreeMap提供了丰富的导航方法:
java
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等操作中都会用到:
java
final int compare(Object k1, Object k2) {
return comparator == null ?
((Comparable<? super K>)k1).compareTo((K)k2) :
comparator.compare((K)k1, (K)k2);
}
如果既没有提供Comparator,键也没有实现Comparable,在插入时会抛出ClassCastException。
4.4 注意事项
-
键不能为null:因为无法比较null
-
compareTo与equals需一致 :当两个键比较结果为0时,TreeMap认为它们相等,即使
equals返回false -
字符串键的特殊性 :字符串的
compareTo基于Unicode值,数字字符串排序时需注意java// 错误:字符串排序按字典序,"22"会排在"5"前面 TreeMap<String, Integer> map = new TreeMap<>(); map.put("5", 1); map.put("22", 2); // 实际顺序:22, 5 // 正确:转为整数比较 TreeMap<String, Integer> map = new TreeMap<>( (a, b) -> Integer.parseInt(a) - Integer.parseInt(b) );
第五章 Hashtable与Properties
5.1 Hashtable:古老的线程安全Map
Hashtable是JDK 1.0就存在的古老实现类,具有以下特点:
- 线程安全 :所有方法都用
synchronized修饰 - 不允许null键和null值:否则抛出NullPointerException
- 初始容量11 ,扩容为
2*old+1 - 性能较低:全表锁导致并发性能差
java
Hashtable<String, Integer> table = new Hashtable<>();
table.put("key", 1);
// table.put(null, 2); // 运行时异常
性能对比:
- 写入速度:Hashtable可能比HashMap快(测试数据:1420ms vs 797ms)
- 读取速度:HashMap比Hashtable快(188ms vs 265ms)
5.2 Properties:处理配置文件
Properties继承自Hashtable,专门用于处理配置文件,键和值都是String类型。
java
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):获取属性,可指定默认值list(PrintStream):打印所有属性
第六章 ConcurrentHashMap:并发编程的利器
ConcurrentHashMap是Java并发包(java.util.concurrent)中提供的线程安全且高性能的Map实现。
6.1 设计哲学
ConcurrentHashMap的设计目标是:在保证线程安全的同时,提供比Hashtable更高的并发性能。
| 实现类 | 锁策略 | 并发度 | 性能 |
|---|---|---|---|
| Hashtable | 全表锁 | 极低 | 差 |
| Collections.synchronizedMap | 全表锁 | 极低 | 差 |
| ConcurrentHashMap JDK 7 | 分段锁 | 16 | 高 |
| ConcurrentHashMap JDK 8+ | CAS + synchronized + 细粒度锁 | 极高 | 非常高 |
6.2 JDK 7实现:分段锁
JDK 7的ConcurrentHashMap采用Segment分段锁机制:
- 将整个Map分成多个Segment(默认16个)
- 每个Segment独立加锁,相当于一个小型的HashMap
- 不同Segment的写操作可以并发执行
- 读操作几乎不加锁(volatile保证可见性)
java
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K,V>[] table;
// ...
}
6.3 JDK 8+实现:CAS + synchronized
JDK 8对ConcurrentHashMap进行了重大重构:
- 放弃分段锁 ,改用CAS + synchronized实现
- 与HashMap结构对齐:数组+链表+红黑树
- 锁粒度更细:只锁住链表或红黑树的头节点
- 读操作完全无锁(volatile保证可见性)
java
// 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); // 帮助扩容
else {
V oldVal = null;
synchronized (f) { // 锁住链表头节点
// 链表或红黑树操作
}
}
}
}
6.4 弱一致性迭代器
ConcurrentHashMap的迭代器是弱一致性的:
- 迭代器创建后,如果Map发生修改,不会抛出
ConcurrentModificationException - 迭代器反映的是创建时刻或之后某个时刻的数据快照
- 迭代过程中修改Map,迭代器可能看到,也可能看不到修改结果
- 适用于高并发场景,避免了快速失败机制带来的问题
6.5 批量操作
ConcurrentHashMap提供了强大的批量操作API:
java
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);
// 用作频率统计(MultiSet)
ConcurrentHashMap<String, LongAdder> freqs = new ConcurrentHashMap<>();
freqs.computeIfAbsent("word", k -> new LongAdder()).increment();
parallelismThreshold参数控制并行度:小于阈值时串行执行,大于阈值时并行执行。
第七章 Map常用方法详解
7.1 基础操作方法
| 方法 | 描述 | 返回值说明 |
|---|---|---|
put(K key, V value) |
添加键值对 | 返回该key之前的value,如果没有则返回null |
get(Object key) |
根据key获取value | 存在则返回value,否则返回null |
remove(Object key) |
删除键值对 | 返回被删除的value |
clear() |
清空所有键值对 | void |
size() |
返回键值对数量 | int |
isEmpty() |
判断是否为空 | boolean |
7.2 查询方法
| 方法 | 描述 |
|---|---|
containsKey(Object key) |
判断是否包含指定键 |
containsValue(Object value) |
判断是否包含指定值(HashMap中效率较低,需遍历) |
getOrDefault(Object key, V defaultValue) |
获取值,不存在则返回默认值 |
7.3 遍历方法
Map的遍历方式多样,可根据场景选择:
7.3.1 entrySet遍历(最常用)
java
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
7.3.2 keySet + get遍历
java
for (String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
// 缺点:每次get都需要二次查找,效率较低
7.3.3 values遍历(仅需值时)
java
for (Integer value : map.values()) {
System.out.println(value);
}
7.3.4 Iterator遍历(支持remove)
java
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(最简洁)
java
map.forEach((key, value) -> System.out.println(key + ": " + value));
7.3.6 Stream API遍历(支持链式操作)
java
map.entrySet().stream()
.filter(entry -> entry.getValue() > 10)
.forEach(entry -> System.out.println(entry.getKey()));
7.4 Java 8+新增的默认方法
Java 8在Map接口中增加了多个实用默认方法,极大地简化了代码:
7.4.1 computeIfAbsent / computeIfPresent
java
// 如果key不存在,则通过函数计算value并放入Map
map.computeIfAbsent("key", k -> new ArrayList<>()).add("value");
// 经典用法:实现多值Map
Map<String, List<String>> multiMap = new HashMap<>();
multiMap.computeIfAbsent("group1", k -> new ArrayList<>()).add("item1");
// 如果key存在,则根据原值计算新值
map.computeIfPresent("key", (k, v) -> v * 2);
7.4.2 merge方法
java
// 合并操作:如果key不存在则放入给定值,存在则通过合并函数计算新值
map.merge("key", 1, Integer::sum); // 统计功能
// 经典用法:单词计数
String text = "apple banana apple orange apple";
Map<String, Integer> wordCount = new HashMap<>();
for (String word : text.split(" ")) {
wordCount.merge(word, 1, Integer::sum);
}
// 结果:{apple=3, banana=1, orange=1}
7.4.3 putIfAbsent
java
// 仅在key不存在时放入
map.putIfAbsent("key", "value");
7.4.4 replace / replaceAll
java
// 替换指定key的值(仅当存在时)
map.replace("key", "newValue");
// 对所有entry应用替换函数
map.replaceAll((k, v) -> v.toUpperCase());
7.5 Java 9的Map.of工厂方法
Java 9提供了更简洁的Map初始化方式:
java
// 创建不可变Map(最多支持10对键值)
Map<String, Integer> map1 = Map.of(
"a", 1,
"b", 2,
"c", 3
);
// 任意数量键值对
Map<String, Integer> map2 = Map.ofEntries(
Map.entry("a", 1),
Map.entry("b", 2),
Map.entry("c", 3),
Map.entry("d", 4)
);
第八章 实现类对比与选型指南
8.1 核心特性对比
| 特性 | HashMap | LinkedHashMap | TreeMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|---|---|
| 顺序 | 无序 | 插入/访问顺序 | 键排序 | 无序 | 无序 |
| null键 | 允许1个 | 允许1个 | 不允许 | 不允许 | 不允许 |
| null值 | 允许 | 允许 | 允许 | 不允许 | 不允许 |
| 线程安全 | 否 | 否 | 否 | 是(全表锁) | 是(分段/CAS) |
| 性能 | 最高 | 略低于HashMap | 较低(log n) | 读慢写快 | 高并发下最优 |
| 底层结构 | 数组+链表+红黑树 | 数组+链表+红黑树+双向链表 | 红黑树 | 数组+链表 | CAS+数组+链表+红黑树 |
| 适用场景 | 通用缓存 | 需保持顺序 | 需排序/范围查询 | 遗留系统 | 高并发共享数据 |
8.2 时间复杂度对比
| 操作 | 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 选型建议
根据不同的业务场景,选择合适的Map实现:
场景1:通用缓存,无特殊顺序要求
- ✅ 首选:HashMap(性能最高)
- 如果线程安全要求:ConcurrentHashMap
场景2:需要保持插入顺序
- ✅ 首选:LinkedHashMap
- 案例:实现FIFO队列、记录操作日志
场景3:需要按键排序或范围查询
- ✅ 首选:TreeMap
- 案例:排行榜、日程表、字典序输出
场景4:实现LRU缓存
- ✅ 首选:LinkedHashMap(访问顺序模式)
- 案例:内存缓存、最近访问记录
场景5:高并发共享数据
- ✅ 首选:ConcurrentHashMap
- 案例:全局配置、在线用户统计
场景6:处理配置文件
- ✅ 首选:Properties
- 案例:读取application.properties
8.4 性能测试数据参考
根据实际测试(百万级数据):
| 操作 | HashMap | LinkedHashMap | TreeMap | Hashtable |
|---|---|---|---|---|
| 插入100万条 | 1420ms | 1512ms | 3845ms | 797ms |
| 读取1000万条 | 188ms | 201ms | 892ms | 265ms |
注:Hashtable插入快可能是由于其初始容量较小,扩容频率高导致的测试偏差,实际应用中HashMap综合性能最优。
第九章 常见陷阱与最佳实践
9.1 陷阱一:可变对象作为键
java
// 错误示例
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
java
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,虽然内容相同
解决方案 :作为键的类必须正确重写hashCode()和equals()。
9.3 陷阱三:并发修改导致ConcurrentModificationException
java
Map<String, Integer> map = new HashMap<>();
// ... 填充数据
for (String key : map.keySet()) {
if (key.startsWith("temp")) {
map.remove(key); // 抛出ConcurrentModificationException
}
}
解决方案:
java
// 方式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(允许并发修改)
9.4 最佳实践总结
-
预估初始容量:如果能预知数据规模,指定初始容量避免频繁扩容
javaMap<String, Integer> map = new HashMap<>(expectedSize * 4 / 3 + 1); -
使用泛型:指定键值类型,避免运行时类型转换异常
-
优先使用Java 8+默认方法:让代码更简洁
java// 老式 if (!map.containsKey(key)) { map.put(key, new ArrayList<>()); } map.get(key).add(value); // 新式 map.computeIfAbsent(key, k -> new ArrayList<>()).add(value); -
选择合适的实现:根据业务需求而非习惯选择
-
注意线程安全:多线程环境优先使用ConcurrentHashMap
-
避免使用Hashtable:除非维护遗留代码
结语
Java Map体系经过多年的演进,从最早的Hashtable,到JDK 1.2引入的HashMap,再到JDK 1.5的ConcurrentHashMap,以及后续的各种优化,已经形成了一套功能完备、性能卓越的数据结构家族。
理解Map的核心原理,不仅有助于写出更高效的代码,还能在遇到复杂业务场景时做出正确的技术选型。本文从源码层面剖析了各个Map实现类的底层机制,并结合实际场景给出了使用建议。在实际开发中,建议遵循"面向接口编程"的原则,根据具体需求选择最合适的Map实现,同时注意线程安全和键的不可变性等关键问题。
Map的学习是一个循序渐进的过程,掌握基础用法后,深入理解其设计思想和源码实现,才能真正做到"知其然,知其所以然"。