一、Map 接口体系结构
Map 接口采用键值对存储模式,其核心实现类的体系结构如下:
Map 接口
├─ HashMap(哈希表实现,JDK8+为数组+链表+红黑树)
├─ Hashtable(哈希表实现,线程安全)
│ └─ Properties(用于配置文件处理)
└─ SortedMap 接口(键有序)
└─ TreeMap(红黑树实现,按键排序)
核心区别:Map 接口与 Collection 接口无继承关系,Collection 存储单一元素,Map 存储键值对;Map 的键(Key)具有无序不可重复特性,值(Value)可重复。
二、HashMap 深度解析
1. 底层存储结构
JDK8 及以后的 HashMap 采用数组 + 链表 + 红黑树的复合结构:
- 数组:作为哈希表的主体,每个元素称为 "桶(Bucket)"(数组下标)
- 链表:解决哈希冲突,当多个键映射到同一桶(数组下标)时形成链表
- 红黑树:当链表长度超过 8 且数组容量≥64 时,链表转为红黑树(检索效率从 O (n) 提升至 O (log n));当红黑树节点数≤6 时,转回链表
2. 关键参数与扩容机制
- 初始化容量:默认 16(建议指定为 2 的倍数,保证哈希分布均匀)
- 加载因子:默认 0.75(元素数量达容量的 75% 时触发扩容)
- 扩容机制:容量翻倍(原容量 ×2)
3. 核心特性
- 线程安全:非线程安全,多线程环境需用
Collections.synchronizedMap
或ConcurrentHashMap
- 键值限制:key 和 value 均可为 null(key 为 null 时仅允许一个)
- 去重逻辑:相同 key 会覆盖旧 value(依赖 key 的
equals
和hashCode
方法)
三、HashMap的put(key,value)实现机制
当我们向 HashMap中添加新元素时,HashMap会按照以下步骤执行:
- 计算新元素key的 hashCode 值,确定其在 HashMap 中的存储位置(数组下标)
- 如果该位置没有元素,则认为该key不重复,直接添加该元素
- 若该位置存在元素则:
- 获取该位置的链表头节点(或红黑树的根节点)
- 遍历链表(或红黑树),对每个节点执行:
- 计算当前元素key的
hash
值是否与新元素key的hash
相等 - 如果
hash
相等(不相等遍历下一个节点),再调用equals
()比较两个元素内容- 如果
equals
返回true
,覆盖该元素键值对的value(终止遍历) - 如果
equals
返回false
,继续遍历下一个节点
- 如果
- 计算当前元素key的
- 遍历完所有节点后:
- 如果未找到相等元素,将新元素插入链表尾部(或红黑树中)
- 如果链表长度超过阈值(默认 8)且数组容量 ≥ 64,将链表转换为红黑树
这里需要特别注意的是,hashCode()和 equals()方法的实现必须遵循以下原则:
- 如果两个对象equals()相等,则它们的 hashCode()值必须相同
- 如果两个对象的 hashCode()值相同,它们equals()不一定相等(哈希冲突)
不重写的后果:
equals()
未重写:即使key相同,集合也会认为是不同元素,导致相同key的键值对元素重复存储。hashCode()
未重写:即使key相同,哈希值也可能不同(导致key相同的键值对元素被存入不同的数组下标而使equals()
比较失效而无法达到键值对元素key不可重复目的,我们应保证key相同时存入相同的数组下标以便后续进行equals()判定
。因此需要根据对象的属性特征值来重写hashCode()方法来计算哈希值)。
四、Map 接口常用方法
方法签名 | 功能描述 |
---|---|
V put(K key, V value) | 添加键值对,返回被覆盖的旧值(无则为 null) |
V get(Object key) | 根据 key 获取 value(无则返回 null) |
V remove(Object key) | 根据 key 删除键值对,返回被删除的 value |
boolean containsKey(Object key) | 判断是否包含指定 key |
boolean containsValue(Object v) | 判断是否包含指定 value |
Set<K> keySet() | 返回所有 key 的 Set 集合 |
Collection<V> values() | 返回所有 value 的 Collection 集合 |
Set<Map.Entry<K,V>> entrySet() | 返回键值对集合(推荐用于遍历) |
五、Map 遍历方式
以 HashMap 为例,演示三种常用遍历方式:
public class Main {
public static void main(String[] args) {
Map<Integer,String> map = new HashMap();
map.put(1,"tom");
map.put(2,"jack");
map.put(3,"Alex");
printWay1(map);
printWay2(map);
printWay3(map);
}
//keySet遍历:需两次哈希查询(keySet一次,get一次)
public static void printWay1(Map map){
Set<Integer> keySet = map.keySet();
for (Integer key : keySet) {
System.out.print(key + "=" + map.get(key) + " ");
}
}
//entrySet遍历:一次获取键值对,效率更高
public static void printWay2(Map map){
Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
for (Map.Entry<Integer, String> entry : entrySet) {
System.out.print(entry.getKey() + "=" + entry.getValue() + " ");
}
}
// Java 8 forEach 遍历
public static void printWay3(Map map){
map.forEach((key, value) -> System.out.print(key + "=" + value + " "));
}
}
效率对比:entrySet
> forEach
> keySet
(keySet
需额外调用get
方法,多一次哈希查询)。
六、Hashtable 特性解析
Hashtable 是早期的哈希表实现,与 HashMap 的核心差异如下:
特性 | HashMap | Hashtable |
---|---|---|
线程安全 | 非线程安全 | 线程安全(方法加synchronized ) |
初始化容量 | 16 | 11 |
扩容机制 | 原容量 ×2 | 原容量 ×2+1 |
键值 null 限制 | 允许 key/value 为 null | 不允许(抛NullPointerException ) |
存储结构 | 数组 + 链表 + 红黑树(JDK8+) | 数组 + 链表 |
注意:Hashtable 因效率低已被淘汰,线程安全场景优先使用ConcurrentHashMap
。
七、Map 实现类选择
场景需求 | 推荐实现类 |
---|---|
一般场景(无序、高效) | HashMap |
线程安全场景 | ConcurrentHashMap |
需按键排序 | TreeMap |
处理配置文件 | Properties |
遗留项目维护 | Hashtable(不推荐新用) |
八、注意事项
-
初始化容量优化:创建 HashMap 时,根据预估元素数量设置初始容量(如
new HashMap<>(100)
),减少扩容次数 -
避免在遍历中修改 Map:增强 for 循环中修改 Map 结构(增删元素)会抛
ConcurrentModificationException
,需用迭代器的remove
方法 -
红黑树转换阈值:JDK8 中,当链表长度 > 8 且数组容量≥64 时转红黑树,节点数 < 6 时转回链表,平衡检索效率
-
null 值处理:HashMap 允许 key 为 null(仅一个),Hashtable 不允许,TreeMap 需注意
compareTo
对 null 的处理
九、总结
实际开发中,需根据业务场景选择合适的实现类,关注 key 的equals
和hashCode
重写,以及遍历方式的效率差异。后续可结合源码深入学习哈希表和红黑树的实现细节,进一步提升 Java 集合框架的掌握程度。