Map 是 Java 集合框架中双列集合 的核心接口,隶属于 java.util 包,与 Collection 接口(单列集合)并列,专门用于存储 "键(Key)- 值(Value)" 映射关系。相较于单列集合,Map 更侧重 "通过键快速查找值" 的场景,其设计思想和底层原理是 Java 集合体系的核心知识点。
一、Map 接口的核心设计原则
1. 键值对的映射规则
- 一对一映射 :一个键(Key)只能关联一个值(Value),向 Map 中插入相同键的新值时,会覆盖原有值,并返回被覆盖的旧值(
put方法的返回值特性)。 - 键的唯一性 :Map 不允许重复的键,"重复" 的判定依赖键对象的
hashCode()和equals()方法 ------ 两个键对象若equals()返回true,且hashCode()返回值相同,则视为同一个键。 - 值的可重复性:值无需满足唯一性,多个不同的键可以映射到同一个值。
2. 空值处理规则
HashMap、LinkedHashMap:允许键为null(仅允许一个),允许值为null(多个);Hashtable、ConcurrentHashMap:不允许键或值为null(避免空指针,适配多线程场景);TreeMap:键不能为null(依赖比较器 / 自然排序,null无法参与比较),值可以为null。
二、Map 核心实现类的底层原理
1. HashMap(JDK 8 及以上)
核心结构
- 底层为 "数组 + 链表 + 红黑树":
- 数组(Node [] table):存储键值对的哈希桶,默认初始容量 16(2^4),扩容因子 0.75,扩容后容量为原 2 倍(保证哈希计算的均匀性);
- 链表:当多个键的哈希值冲突(映射到同一数组下标)时,先以链表形式存储,链表长度阈值为 8;
- 红黑树:当链表长度 ≥ 8 且数组容量 ≥ 64 时,链表转为红黑树(降低查找时间复杂度:从 O (n) 到 O (logn));若后续删除元素导致树节点数 ≤ 6,红黑树转回链表。
哈希计算与索引定位
- 计算键的
hashCode(); - 对哈希值进行扰动处理:
hash = key.hashCode() ^ (key.hashCode() >>> 16)(高位参与运算,减少哈希冲突); - 计算数组下标:
index = hash & (table.length - 1)(等价于取模,效率更高,前提是数组容量为 2 的幂)。
线程安全性
- 非线程安全:多线程下扩容(
resize())可能导致链表成环,插入 / 删除操作可能出现数据丢失; - 解决方式:使用
ConcurrentHashMap(JDK 8 后基于 CAS + synchronized 实现,性能优于Hashtable)。
2. LinkedHashMap
核心特性
- 继承自
HashMap,底层额外维护一个双向链表,记录键值对的插入顺序 / 访问顺序; - 有序性:默认按 "插入顺序" 遍历,也可通过构造器指定
accessOrder = true实现 "访问顺序"(每次get/put访问的元素会移到链表尾部,适用于实现 LRU 缓存)。
底层实现
- 重写
HashMap的Node节点,新增before和after指针,用于构建双向链表; - 重写
newNode()、afterNodeAccess()、afterNodeInsertion()等方法,维护双向链表的节点顺序。
3. TreeMap
核心结构
- 底层为红黑树(自平衡的二叉查找树),无哈希表结构,因此无需考虑哈希冲突;
- 有序性:按键的 "自然顺序"(如 Integer 升序、String 字典序)或自定义
Comparator排序,遍历结果始终有序。
键的要求
- 键必须实现
Comparable接口(自然排序),或创建TreeMap时指定Comparator(自定义排序); - 若键未实现
Comparable且无自定义比较器,调用put时会抛出ClassCastException。
核心方法
ceilingKey(K key):返回大于等于指定键的最小键;floorKey(K key):返回小于等于指定键的最大键;subMap(K fromKey, K toKey):获取键在指定范围的子 Map(视图,原 Map 变更会同步)。
4. Hashtable(古老实现,几乎淘汰)
- 底层为 "数组 + 链表"(无红黑树优化);
- 线程安全:所有方法加
synchronized锁(锁整个对象,并发效率低); - 容量:默认初始容量 11(非 2 的幂),扩容因子 0.75,扩容后容量 = 原容量 × 2 + 1;
- 已被
ConcurrentHashMap替代,仅兼容旧代码场景。
5. ConcurrentHashMap
JDK 8 核心实现
- 底层为 "数组 + 链表 + 红黑树",与
HashMap结构类似; - 线程安全:
- 数组分段锁:不再使用 JDK 7 的分段锁(Segment),改为对数组单个桶(Node)加
synchronized锁(锁粒度更小,并发效率更高); - CAS 操作:对桶的初始化、节点的插入等无冲突操作,使用 CAS 保证原子性,避免加锁。
- 数组分段锁:不再使用 JDK 7 的分段锁(Segment),改为对数组单个桶(Node)加
- 不允许键 / 值为
null,避免多线程下null值的歧义(无法区分 "键不存在" 和 "值为 null")。
三、Map 的遍历方式与性能分析
1. 核心遍历方式
| 遍历方式 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 键遍历(keySet ()) | for (K key : map.keySet()) { V value = map.get(key); } |
代码简洁 | 需两次哈希查找(keySet 遍历一次,get 一次),效率低 |
| 键值对遍历(entrySet ()) | for (Map.Entry<K,V> entry : map.entrySet()) { K key = entry.getKey(); V value = entry.getValue(); } |
一次遍历获取键值,效率最高 | 代码稍繁琐 |
| 迭代器遍历(Iterator) | Iterator<Map.Entry<K,V>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { ... } |
支持遍历中删除元素(iterator.remove ()) | 代码繁琐 |
| forEach 遍历(JDK 8+) | map.forEach((k, v) -> { ... }); |
函数式编程,代码简洁 | 无法遍历中删除元素(需用 removeIf) |
2. 性能对比
- 最优:
entrySet()遍历(仅一次哈希计算,直接获取键值对); - 次优:forEach 遍历(底层基于 entrySet,语法简化);
- 最差:
keySet()+get()(两次哈希查找,哈希冲突时性能更差)。
四、自定义对象作为 Map 键的注意事项
1. 必须重写 hashCode() 和 equals()
- 原因:Map 依赖这两个方法判断键的唯一性,若不重写,默认使用 Object 类的实现:
equals():比较对象地址,导致即使内容相同的两个对象被视为不同键;hashCode():返回对象的内存地址哈希值,导致相同内容的对象哈希值不同,映射到不同数组桶。
2. 重写原则
- 一致性:若两个对象
equals()返回true,则hashCode()必须返回相同值;若equals()返回false,hashCode()尽量返回不同值(减少哈希冲突); - 稳定性:
hashCode()的返回值在对象生命周期内不能改变(若键对象的属性参与哈希计算,需保证属性不可变,或避免修改参与计算的属性)。
3. 示例代码
java
class Student {
private String id;
private String name;
// 构造器、getter/setter 省略
// 重写 equals()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(id, student.id); // 以唯一标识 id 作为判断依据
}
// 重写 hashCode()
@Override
public int hashCode() {
return Objects.hash(id); // 仅基于 id 计算哈希值
}
}
// 使用自定义对象作为键
Map<Student, Integer> scoreMap = new HashMap<>();
scoreMap.put(new Student("001", "张三"), 90);
scoreMap.put(new Student("001", "张三"), 95); // 覆盖原有值(equals 返回 true)
System.out.println(scoreMap.size()); // 输出 1
五、Map 的扩容机制(以 HashMap 为例)
1. 扩容触发条件
- 当 Map 中键值对数量(size)≥ 数组容量 × 扩容因子(默认 16 × 0.75 = 12)时,触发扩容;
- 当链表转红黑树时,若数组容量 < 64,先扩容数组(而非转树),避免小容量下频繁转树。
2. 扩容流程
- 创建新数组,容量为原数组的 2 倍;
- 遍历原数组的所有节点,重新计算节点在新数组的下标(因容量翻倍,下标仅可能为原下标或原下标 + 原容量);
- 将节点迁移到新数组(红黑树节点需拆分,链表节点直接迁移);
- 替换原数组为新数组,完成扩容。
六、Map 与 Collection 的核心区别
| 维度 | Map | Collection |
|---|---|---|
| 存储形式 | 键值对(双列) | 单个元素(单列) |
| 核心子接口 | HashMap、TreeMap、ConcurrentHashMap 等 | List、Set、Queue 等 |
| 元素唯一性 | 键唯一,值可重复 | Set 元素唯一,List 元素可重复 |
| 遍历方式 | 键遍历、键值对遍历、值遍历 | 直接遍历、迭代器遍历 |
| 核心方法 | put、get、remove(按键操作) | add、remove、contains(按元素操作) |
七、常见面试考点总结
- HashMap JDK 7 与 JDK 8 的区别:JDK 7 为 "数组 + 链表",JDK 8 新增红黑树优化,哈希计算扰动方式简化,扩容迁移逻辑优化;
- HashMap 为什么线程不安全:扩容成环、数据丢失、可见性问题;
- ConcurrentHashMap 线程安全实现方式:JDK 7 分段锁(Segment),JDK 8 CAS + 桶级 synchronized;
- TreeMap 排序原理:基于红黑树的自然排序 / 自定义比较器;
- LinkedHashMap 实现 LRU 缓存:通过
accessOrder = true让访问过的元素移到链表尾部,淘汰链表头部元素。
总结
Map 是 Java 中处理键值对映射的核心工具,不同实现类适配不同场景:
- 无顺序要求、追求高性能:
HashMap; - 需保持插入 / 访问顺序:
LinkedHashMap; - 需按键排序:
TreeMap; - 多线程并发场景:
ConcurrentHashMap; - 避免使用
Hashtable(性能差、API 老旧)。掌握 Map 的底层原理和使用规范,是优化 Java 程序性能、避免并发问题的关键。