一、HashMap 深度解析(补充底层核心原理)
1. 存储结构:数组 + 链表 + 红黑树(JDK1.8 优化)
HashMap 的底层是「哈希桶数组」,每个桶对应一个链表 / 红黑树,核心结构如下:
- 哈希桶数组 :默认初始容量 16(2 的幂次),数组下标 =
key.hashCode() ^ (key.hashCode() >>> 16) & (capacity - 1)(扰动函数 + 与运算,减少哈希冲突); - 链表:当多个 key 哈希到同一个桶时,形成链表(JDK1.7 头插法,JDK1.8 尾插法,避免并发死循环);
- 红黑树:当链表长度 ≥ 8 且数组容量 ≥ 64 时,链表转为红黑树(查询效率从 O (n) 优化为 O (log n));当链表长度 ≤ 6 时,红黑树转回链表。
2. 扩容机制(面试高频)
- 触发条件 :元素个数
size > capacity × loadFactor(默认负载因子 0.75,即 16×0.75=12 时触发扩容); - 扩容规则:新容量 = 旧容量 × 2(保证是 2 的幂次),重新计算所有元素的哈希桶位置(rehash);
- 性能优化 :创建 HashMap 时指定初始容量(如已知存储 1000 个元素,指定
new HashMap<>(1024)),避免频繁扩容。
3. 核心易错点
(1)key 为 null 的处理
HashMap 允许 key 为 null,底层会将 null 键的哈希值固定为 0,存入数组下标 0 的桶中:
java
运行
HashMap<String, Integer> map = new HashMap<>();
map.put(null, 100);
System.out.println(map.get(null)); // 输出 100(正常访问)
(2)并发安全问题
HashMap 非线程安全,多线程环境下扩容可能导致死循环(JDK1.7)、数据丢失(JDK1.8),严禁在多线程中直接使用:
- 错误示例:多线程 put 元素可能导致
ConcurrentModificationException或数据不一致; - 正确方案:使用
ConcurrentHashMap(分段锁 / CAS + synchronized,并发效率远高于 Hashtable)。
(3)自定义对象作为 key 的坑
自定义对象作为 key 时,必须重写 hashCode() 和 equals(),否则会导致 key 重复:
java
运行
// 反例:未重写hashCode/equals
class User {
private String id;
public User(String id) { this.id = id; }
}
HashMap<User, String> map = new HashMap<>();
map.put(new User("001"), "张三");
System.out.println(map.get(new User("001"))); // 输出 null(两个User对象哈希值不同)
// 正例:重写hashCode/equals
class User {
private String id;
public User(String id) { this.id = id; }
@Override
public int hashCode() {
return id.hashCode(); // 基于唯一标识id计算哈希
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
}
4. 遍历方式对比(实战推荐)
表格
| 遍历方式 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| entrySet 遍历 | 一次获取键值对,效率高 | 代码稍长 | ★★★★★ |
| keySet 遍历 | 仅遍历键,代码简洁 | 需二次 get (value),效率低 | ★★★☆☆ |
| forEach(Java8+) | 代码极简 | 无法中途退出 | ★★★★☆ |
java
运行
HashMap<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
// 推荐:entrySet遍历
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " → " + entry.getValue());
}
// Java8+ 极简遍历
map.forEach((k, v) -> System.out.println(k + " → " + v));
二、TreeMap 深度解析(补充排序核心)
1. 红黑树的核心特性
TreeMap 基于红黑树(自平衡二叉搜索树) 实现,保证:
- 左子树所有节点 <根节点,右子树所有节点> 根节点;
- 任意节点的左右子树高度差 ≤ 1(平衡);
- 增删改查效率稳定在 O (log n)(优于链表,劣于 HashMap)。
2. 排序的两种方式(补充细节)
(1)自然排序(Comparable)
自定义类作为 key 时,实现 Comparable 接口:
java
运行
class Student implements Comparable<Student> {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
// 按分数降序排序
@Override
public int compareTo(Student o) {
return o.score - this.score; // 降序;升序:this.score - o.score
}
@Override
public String toString() {
return name + "(" + score + ")";
}
}
public class TreeMapDemo {
public static void main(String[] args) {
TreeMap<Student, String> map = new TreeMap<>();
map.put(new Student("张三", 85), "一班");
map.put(new Student("李四", 92), "二班");
map.put(new Student("王五", 78), "三班");
// 输出:李四(92)→二班, 张三(85)→一班, 王五(78)→三班
map.forEach((k, v) -> System.out.println(k + "→" + v));
}
}
(2)自定义排序(Comparator)
优先于自然排序,适合无法修改 key 类源码的场景:
java
运行
// 按姓名长度排序(匿名内部类 → Lambda 简化)
TreeMap<Student, String> map = new TreeMap<>((s1, s2) -> {
return s1.getName().length() - s2.getName().length();
});
3. 核心限制
- key 不能为 null:红黑树排序时会调用
key.compareTo(),null 调用方法会抛NullPointerException; - 性能低于 HashMap:O (log n) 对比 O (1),仅在需要排序时使用。
三、Hashtable 深度解析(补充现代替代方案)
1. 核心缺陷(为何被淘汰)
- 线程安全实现低效 :所有方法加
synchronized关键字(对象级锁),即使多线程操作不同桶,也会互斥,并发性能极差; - 无红黑树优化:始终是数组 + 链表,哈希冲突严重时查询效率低;
- 不支持 null 键值:put null 会直接抛异常;
- 扩容规则不友好:新容量 = 旧容量 ×2 +1(非 2 的幂次),哈希分布不如 HashMap 均匀。
2. 现代替代方案(实战首选)
表格
| 场景 | 替代方案 | 核心优势 |
|---|---|---|
| 单线程 + 无需排序 | HashMap | 效率最高 |
| 单线程 + 需要排序 | TreeMap | 有序遍历 |
| 多线程 + 高并发 | ConcurrentHashMap | 分段锁 / CAS 实现,并发效率高 |
| 多线程 + 简单场景 | Collections.synchronizedMap(new HashMap<>()) | 快速适配,无需引入并发包 |
ConcurrentHashMap 示例(推荐)
java
运行
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentMapDemo {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 多线程安全插入
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
map.put(Thread.currentThread().getName() + "-" + i, i);
}
};
Thread t1 = new Thread(task, "线程1");
Thread t2 = new Thread(task, "线程2");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终元素个数:" + map.size()); // 2000(无数据丢失)
}
}
四、三大 Map 类增强对比表
表格
| 特性 | HashMap | TreeMap | Hashtable |
|---|---|---|---|
| 底层结构 | 数组 + 链表 + 红黑树(JDK1.8+) | 红黑树 | 数组 + 链表(无红黑树) |
| 时间复杂度 | 增删改查 O (1)(平均) | 增删改查 O (log n) | 增删改查 O (1)(平均,同步开销大) |
| 有序性 | 无序(按哈希桶分布) | 有序(按键排序) | 无序 |
| 线程安全 | 否 | 否 | 是(对象级锁,低效) |
| null 支持 | key/value 均可为 null | key 不可为 null,value 可为 null | key/value 均不可为 null |
| 初始容量 / 扩容 | 16 / 2 倍 | 无初始容量(红黑树动态增长) | 11 / 2 倍 + 1 |
| 迭代器类型 | Iterator(fail-fast) | Iterator(fail-fast) | Enumeration/Iterator |
| 现代替代方案 | -(单线程首选) | -(有序场景首选) | ConcurrentHashMap |
| 适用场景 | 单线程、高效、无需排序 | 单线程、需要按键排序 | 仅老系统维护,新开发禁用 |
五、实战选型决策树(快速选对 Map)
flowchart TD
A[选择Map实现类] --> B{是否多线程环境?}
B -->|是| C{高并发?}
C -->|是| D[ConcurrentHashMap]
C -->|否| E[Collections.synchronizedMap]
B -->|否| F{是否需要按键排序?}
F -->|是| G[TreeMap]
F -->|否| H[HashMap]
六、开发最佳实践
-
优先用 HashMap:90% 的单线程场景首选,创建时指定初始容量优化性能;
-
自定义 key 必须重写 hashCode/equals:否则会导致 key 重复或查询不到;
-
多线程禁用 Hashtable:优先用 ConcurrentHashMap(JDK1.8+ 性能大幅优化);
-
TreeMap 仅用于排序场景:如排行榜、字典序遍历,非排序场景用 HashMap;
-
避免遍历中修改 Map :遍历删除用
iterator.remove()或map.removeIf(),避免ConcurrentModificationException; -
Java8+ 推荐用 Map.of () 创建不可变 Map :简化少量元素的初始化:
java
运行
// 不可变Map(键值不可增删改) Map<String, Integer> immutableMap = Map.of("A", 1, "B", 2, "C", 3);
总结
- HashMap:基于哈希表,无序、高效、支持 null,单线程非排序场景首选,核心优化点是指定初始容量和重写 key 的 hashCode/equals;
- TreeMap:基于红黑树,有序、不支持 null key,仅适用于单线程按键排序的场景;
- Hashtable:古老的线程安全实现,效率极低,现代开发中已被 ConcurrentHashMap 完全替代;
- 选型核心:单线程看是否需要排序,多线程看并发强度(高并发用 ConcurrentHashMap)。