Java Map集合核心知识点总结(HashMap/TreeMap/Hashtable/ConcurrentHashMap)
在Java集合框架中,Map是与Collection并列的顶级接口,代表键值对(key-value) 形式的存储结构,键(key)唯一不可重复,值(value)可重复,且一个键只能映射一个值。本文将从底层原理、源码解析、性能对比、面试陷阱等维度深入梳理Map的四大核心实现类,帮你彻底掌握Map集合的使用与选型。
一、HashMap:最常用的无序哈希表实现
1. 核心特点
- 底层基于哈希表 实现,JDK1.7为数组+单向链表 ,JDK1.8优化为数组+链表+红黑树
- 无序:不保证元素的插入顺序,也不保证顺序随时间不变
- 允许一个null键 和多个null值(null键固定存放在数组索引0的位置)
- 线程不安全:多线程环境下操作会出现并发修改异常,甚至导致数据结构损坏
- 查找、插入、删除的平均时间复杂度为O(1),是综合性能最高的Map实现
2. 底层原理与核心机制(重点)
(1)存储结构(JDK1.8)
- 数组(Node<K,V>[] table):哈希表的主体,初始默认容量为16,必须是2的幂次方
- 链表:解决哈希冲突,当多个key的哈希值相同时,以链表形式存储在同一个数组位置
- 红黑树:当链表长度≥8且数组容量≥64时,链表自动转换为红黑树,将查询时间复杂度从O(n)优化为O(logn);当红黑树节点数≤6时,自动退化为链表
(2)哈希计算与索引定位
java
// JDK1.8哈希计算:key的hashCode()异或其高16位,减少哈希冲突
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 索引定位:哈希值 & (数组长度 - 1),等价于取模运算但效率更高
int index = hash(key) & (table.length - 1);
(3)扩容机制
- 触发条件:当元素数量(size)超过容量 × 负载因子(默认0.75)时触发扩容
- 扩容规则:新容量为原容量的2倍(保证2的幂次方特性)
- 扩容过程:创建新的数组,将原数组的所有键值对重新哈希并分配到新数组中,这是HashMap的主要性能瓶颈
- 特殊情况:如果原数组容量已达到
MAXIMUM_CAPACITY(2^30),则不再扩容,将负载因子设置为1.0
(4)put方法核心流程
- 计算key的哈希值,定位数组索引
- 如果数组该位置为空,直接创建新节点插入
- 如果该位置已有节点:
- 若key已存在(哈希值相同且equals返回true),则覆盖旧值
- 若节点是红黑树节点,调用红黑树的插入方法
- 若节点是链表节点,遍历链表插入尾部;若插入后链表长度≥8,触发树化检查
3. 常用方法与示例
(1)添加与修改元素
java
HashMap<String, Integer> map = new HashMap<>();
// 添加键值对,若key已存在则覆盖旧值,返回旧值
map.put("Tom", 100);
map.put("Jim", 90);
// 批量添加另一个Map的所有键值对
HashMap<String, Integer> map1 = new HashMap<>();
map1.put("Sam", 91);
map.putAll(map1);
// 仅当key不存在时才添加,返回null表示添加成功
map.putIfAbsent("Tom", 95);
// 替换指定key的value,返回旧值
map.replace("Jim", 95);
(2)查询元素
java
// 根据key获取value,不存在则返回null
int score = map.get("Tom");
// 根据key获取value,不存在则返回默认值
int defaultScore = map.getOrDefault("Bob", 0);
// 判断是否包含指定key
boolean hasTom = map.containsKey("Tom");
// 判断是否包含指定value
boolean has90 = map.containsValue(90);
// 获取键值对数量
int size = map.size();
// 判断是否为空
boolean isEmpty = map.isEmpty();
(3)删除元素
java
// 根据key删除键值对,返回被删除的value
map.remove("Tom");
// 仅当key和value都匹配时才删除,返回boolean
map.remove("Jim", 90);
// 清空所有键值对
map.clear();
(4)三种遍历方式
java
// 1. 遍历key集合(适合只需要key的场景)
for (String key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
// 2. 遍历value集合(适合只需要value的场景)
for (Integer value : map.values()) {
System.out.println(value);
}
// 3. 遍历entrySet(推荐,性能最高,避免二次get)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
4. 常见陷阱与注意事项
- 必须重写key的hashCode()和equals()方法:HashMap通过这两个方法判断key是否相等,若不重写会导致相同内容的key被视为不同对象
- 避免使用可变对象作为key:如果key的内容发生变化,其hashCode也会变化,导致无法找到对应的value
- JDK1.7 HashMap死循环问题:JDK1.7采用头插法扩容,多线程环境下可能导致链表成环,引发死循环;JDK1.8改为尾插法解决了该问题,但仍不保证线程安全
- 初始容量优化 :如果提前知道元素数量,创建HashMap时指定初始容量(建议设置为
预计元素数 / 0.75 + 1),避免频繁扩容
二、TreeMap:基于红黑树的有序Map实现
1. 核心特点
- 底层基于红黑树实现,所有键值对按照key的排序规则存储
- 有序:支持自然排序和自定义排序,是唯一实现了SortedMap接口的常用Map
- 键不允许为null(需要参与排序比较),值可以为null
- 线程不安全:多线程环境下操作会出现并发修改异常
- 查找、插入、删除的时间复杂度均为O(logn),性能低于HashMap
2. 排序规则
TreeMap提供两种排序方式,优先级:自定义排序 > 自然排序
- 自然排序 :key必须实现
Comparable接口,重写compareTo()方法。String、Integer等包装类已默认实现该接口 - 自定义排序 :创建TreeMap时传入
Comparator接口的实现类,重写compare()方法
3. 常用方法与示例
(1)基本使用
java
// 自然排序:String按字典序,Integer按数值升序
TreeMap<String, Integer> map = new TreeMap<>();
map.put("orange", 1);
map.put("apple", 2);
map.put("pear", 3);
System.out.println(map); // 输出:{apple=2, orange=1, pear=3}
// 自定义排序:按key的长度升序
TreeMap<String, Integer> customMap = new TreeMap<>((s1, s2) -> s1.length() - s2.length());
customMap.put("banana", 4);
customMap.put("apple", 2);
customMap.put("pear", 3);
System.out.println(customMap); // 输出:{pear=3, apple=2, banana=4}
(2)特有方法(有序性相关)
java
// 获取第一个(最小)key和最后一个(最大)key
String firstKey = map.firstKey();
String lastKey = map.lastKey();
// 获取第一个和最后一个键值对
Map.Entry<String, Integer> firstEntry = map.firstEntry();
Map.Entry<String, Integer> lastEntry = map.lastEntry();
// 获取小于等于指定key的最大键值对
Map.Entry<String, Integer> floorEntry = map.floorEntry("orange");
// 获取大于等于指定key的最小键值对
Map.Entry<String, Integer> ceilingEntry = map.ceilingEntry("orange");
// 获取子Map:[fromKey, toKey)
SortedMap<String, Integer> subMap = map.subMap("apple", "pear");
// 获取小于toKey的子Map
SortedMap<String, Integer> headMap = map.headMap("orange");
// 获取大于等于fromKey的子Map
SortedMap<String, Integer> tailMap = map.tailMap("orange");
4. 注意事项
- 排序规则必须与
equals()方法保持一致,否则会出现"equals返回true但compareTo返回非0"的情况,导致TreeMap行为异常 - 由于基于红黑树实现,插入和删除操作需要维护红黑树的平衡,因此性能低于HashMap,仅在需要有序遍历的场景下使用
三、Hashtable:已过时的线程安全哈希表
1. 核心特点
- 底层基于哈希表实现,结构与JDK1.7 HashMap类似(数组+单向链表)
- 线程安全 :所有方法都添加了
synchronized同步锁,但锁粒度大,性能极差 - 键和值都不允许为null ,否则会抛出
NullPointerException - 默认初始容量为11,负载因子0.75,扩容规则为新容量 = 旧容量 × 2 + 1
- 不建议使用 :属于JDK1.0的遗留类,设计老旧且性能差,如需线程安全的Map,推荐使用
ConcurrentHashMap替代
2. 与HashMap的核心区别
| 特性 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | 不安全 | 安全(方法级加锁) |
| null键值 | 允许1个null键,多个null值 | 不允许任何null键和值 |
| 初始容量 | 16 | 11 |
| 扩容规则 | 2倍 | 2倍 + 1 |
| 底层结构 | 数组+链表+红黑树(JDK1.8) | 数组+链表 |
| 性能 | 高 | 低(同步开销大) |
| 迭代器类型 | 快速失败(fail-fast) | 快速失败(fail-fast) |
3. 常用方法(了解即可)
Hashtable的方法与HashMap基本一致,同时提供了一些历史遗留方法:
elements():返回value的Enumeration迭代器keys():返回key的Enumeration迭代器contains(Object value):功能等价于containsValue(),命名不规范
四、ConcurrentHashMap:高并发场景的首选
1. 核心原理
ConcurrentHashMap是java.util.concurrent包下的线程安全Map实现,不同JDK版本的实现差异较大:
- JDK1.7:采用**分段锁(Segment)**机制,将哈希表分为16个段,每个段独立加锁,支持16个线程并发写
- JDK1.8 :取消分段锁,采用数组+链表+红黑树+CAS+synchronized机制,锁粒度降低到单个数组节点,并发性能大幅提升
2. 核心特性
- 线程安全:通过CAS和synchronized保证并发操作的安全性
- 高并发:读操作完全无锁,写操作仅锁定当前节点,支持大量线程并发访问
- 不允许null键和null值(与Hashtable一致)
- 初始容量16,负载因子0.75,扩容规则与HashMap相同(2倍)
- 迭代器是弱一致性迭代器,遍历过程中允许修改集合,不会抛出并发修改异常
3. 优缺点与适用场景
- 优点 :并发性能远高于Hashtable和
Collections.synchronizedMap,是高并发场景下的标准选择 - 缺点:弱一致性迭代器只能保证看到遍历开始时已存在的数据,不保证看到后续的修改
- 适用场景:高并发读多写少场景,如缓存、计数器、路由表等
五、Map集合核心总结与选型对比
1. Map接口通用特性
- 存储键值对,键唯一不可重复,值可重复
- 一个键只能映射一个值,重复添加相同键会覆盖旧值
- 支持通过key快速查找value,是查找效率最高的数据结构之一
- 所有实现类都提供了添加、删除、查询、遍历的基本方法
2. 四大实现类对比表
| 特性 | HashMap | TreeMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|---|
| 底层数据结构 | 数组+链表+红黑树(JDK1.8) | 红黑树 | 数组+链表 | 数组+链表+红黑树+CAS(JDK1.8) |
| 有序性 | 无序 | 有序(按key排序) | 无序 | 无序 |
| null键值 | 允许1个null键,多个null值 | 不允许null键,允许null值 | 不允许任何null键和值 | 不允许任何null键和值 |
| 线程安全 | 不安全 | 不安全 | 安全(方法级加锁) | 安全(细粒度锁) |
| 平均时间复杂度 | O(1) | O(logn) | O(1) | O(1) |
| 并发性能 | 无 | 无 | 极低 | 极高 |
| 适用场景 | 单线程通用场景 | 需要有序遍历的场景 | 无(仅维护老代码) | 高并发读多写少场景 |
| 综合性能 | 最高 | 中等 | 最低 | 高(并发场景) |
3. 补充注意事项
- 并发安全方案 :
- 轻度并发场景:使用
Collections.synchronizedMap(Map map)包装HashMap - 高并发场景:必须使用
ConcurrentHashMap,绝对不要使用Hashtable
- 轻度并发场景:使用
- 遍历性能选择 :
- 所有Map实现类都推荐使用
entrySet()遍历,避免使用keySet()遍历后再调用get()获取value - 不要在遍历过程中使用Map的
remove()方法删除元素,应使用迭代器的remove()方法
- 所有Map实现类都推荐使用
- Collections工具类常用方法 :
Collections.unmodifiableMap(Map map):返回不可修改的Map视图,防止误修改Collections.synchronizedMap(Map map):返回线程安全的Map包装类Collections.emptyMap():返回一个空的不可变Map
结论
Map集合是Java开发中处理键值对数据的核心工具,掌握其底层原理和选型技巧能显著提升代码质量和运行性能。日常开发中,绝大多数单线程场景优先使用HashMap;当需要对键值对进行有序排序和遍历的时候,选择TreeMap;Hashtable由于性能和设计缺陷,除非是维护遗留老代码,否则坚决不要使用;在高并发场景下,ConcurrentHashMap是唯一的标准选择。