数据结构-HashMap
在 Java 键值对(Key-Value)集合中,HashMap 是使用频率最高的实现类之一,凭借高效的查找、插入性能,成为日常开发的 “利器”。本文将从 HashMap 的底层原理、核心特点、常用方法到遍历方式、使用注意事项,进行系统性梳理,帮助快速掌握其核心逻辑与实战技巧。
一、HashMap 核心认知:底层原理与特点
HashMap 本质是哈希表(数组 + 链表 / 红黑树) 实现的键值对存储结构,核心目标是通过 “哈希算法” 快速定位元素,平衡查询与增删效率。其核心特点如下:
1. 底层存储结构(JDK 1.8+)
- 基础结构:数组(称为 “哈希桶”)+ 链表 + 红黑树。
- 数组:每个元素是一个 “链表头节点”,通过
key
的哈希值计算数组索引(index = (数组长度 - 1) & 哈希值
),实现快速定位。 - 链表:当多个
key
计算出相同索引(哈希冲突)时,元素以链表形式存储在对应桶中。 - 红黑树:当链表长度超过 8 且数组长度 ≥ 64 时,链表会转为红黑树,将查询时间复杂度从 O (n) 优化为 O (log n)(避免链表过长导致性能下降)。
- 数组:每个元素是一个 “链表头节点”,通过
- 示意图简化理解:
数组索引 0:[Node(key1, val1)] → [Node(key2, val2)] // 链表(长度<8)
数组索引 1:[TreeNode(key3, val3)] → 红黑树结构 // 红黑树(长度≥8)
数组索引 2:null
...
2. 核心特点
- 键值对规则:
- Key 唯一:若添加重复 Key,新 Value 会覆盖旧 Value(
put()
方法返回旧 Value)。 - Value 可重复:不同 Key 可对应相同 Value。
- 允许 null:Key 最多允许 1 个 null(重复添加 null Key 会覆盖),Value 可多个 null。
- Key 唯一:若添加重复 Key,新 Value 会覆盖旧 Value(
- 无序性:存储顺序与插入顺序无关(底层按哈希值排序,非插入顺序)。
- 线程不安全:非同步设计,多线程同时读写可能出现数据异常(如
ConcurrentModificationException
),需手动处理线程安全(如Collections.synchronizedMap()
或ConcurrentHashMap
)。 - 自动扩容:
- 默认初始容量:16(数组长度,必须是 2 的幂,确保哈希计算均匀)。
- 负载因子:默认 0.75(当元素数量 ≥ 容量 × 负载因子时触发扩容)。
- 扩容规则:新容量 = 旧容量 × 2,同时重新计算所有元素的哈希索引(“rehash”),会消耗一定性能。
- 高效性能:
- 理想情况下,插入、查询、删除的时间复杂度均为 O(1)(直接通过哈希值定位桶)。
- 哈希冲突较少时,性能接近理想值;冲突严重(链表过长)时,性能会下降(红黑树优化可缓解此问题)。
二、HashMap 常用方法
HashMap 提供了丰富的方法操作键值对,以下是开发中最常用的方法,均附完整示例代码,可直接复制运行。
1. 基础操作:添加、获取、删除
(1)添加键值对(put ())
V put(K key, V value)
:添加键值对,若 Key 已存在则覆盖 Value,返回旧 Value(若 Key 不存在则返回 null)。void putAll(Map<? extends K, ? extends V> m)
:将另一个同类型 Map 的所有键值对添加到当前 HashMap 中(重复 Key 会被覆盖)。
import java.util.HashMap;public class HashMapPutDemo {public static void main(String[] args) {// 1. 单个键值对添加HashMap<String, Integer> scoreMap = new HashMap<>();Integer oldMathScore = scoreMap.put("数学", 90); // Key 不存在,返回 nullSystem.out.println("旧数学成绩:" + oldMathScore); // 输出:nullscoreMap.put("语文", 85);scoreMap.put("英语", 95);System.out.println("添加后:" + scoreMap); // 输出:{数学=90, 语文=85, 英语=95}// 重复 Key 覆盖:数学成绩从 90 改为 98Integer updatedOldScore = scoreMap.put("数学", 98);System.out.println("被覆盖的旧数学成绩:" + updatedOldScore); // 输出:90System.out.println("覆盖后:" + scoreMap); // 输出:{数学=98, 语文=85, 英语=95}// 2. 批量添加(putAll())HashMap<String, Integer> extraScoreMap = new HashMap<>();extraScoreMap.put("物理", 88);extraScoreMap.put("化学", 92);scoreMap.putAll(extraScoreMap);System.out.println("批量添加后:" + scoreMap); // 输出:{数学=98, 语文=85, 英语=95, 物理=88, 化学=92}}
}
(2)获取值与判断存在(get ()、containsKey ()、containsValue ())
V get(Object key)
:根据 Key 获取 Value,若 Key 不存在则返回 null(注意:若 Value 本身是 null,需用containsKey()
区分 “Key 不存在” 和 “Value 为 null”)。boolean containsKey(Object key)
:判断 HashMap 是否包含指定 Key,返回布尔值。boolean containsValue(Object value)
:判断 HashMap 是否包含指定 Value,返回布尔值。
public class HashMapGetContainsDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("语文", 85);scoreMap.put("生物", null); // Value 为 null// 1. 根据 Key 获取 ValueInteger mathScore = scoreMap.get("数学");Integer historyScore = scoreMap.get("历史"); // Key 不存在Integer bioScore = scoreMap.get("生物"); // Value 本身是 nullSystem.out.println("数学成绩:" + mathScore); // 输出:98System.out.println("历史成绩(Key 不存在):" + historyScore); // 输出:nullSystem.out.println("生物成绩(Value 为 null):" + bioScore); // 输出:null// 2. 判断 Key 是否存在(区分“Key 不存在”和“Value 为 null”)boolean hasBioKey = scoreMap.containsKey("生物");boolean hasHistoryKey = scoreMap.containsKey("历史");System.out.println("是否包含 Key '生物':" + hasBioKey); // 输出:trueSystem.out.println("是否包含 Key '历史':" + hasHistoryKey); // 输出:false// 3. 判断 Value 是否存在boolean has98 = scoreMap.containsValue(98);boolean has100 = scoreMap.containsValue(100);System.out.println("是否包含 Value 98:" + has98); // 输出:trueSystem.out.println("是否包含 Value 100:" + has100); // 输出:false}
}
(3)删除键值对(remove ())
V remove(Object key)
:根据 Key 删除键值对,返回被删除的 Value(若 Key 不存在则返回 null)。
public class HashMapRemoveDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("语文", 85);scoreMap.put("英语", 95);// 删除 Key 为“语文”的键值对Integer removedChineseScore = scoreMap.remove("语文");System.out.println("被删除的语文成绩:" + removedChineseScore); // 输出:85System.out.println("删除后:" + scoreMap); // 输出:{数学=98, 英语=95}// 删除不存在的 KeyInteger removedHistoryScore = scoreMap.remove("历史");System.out.println("删除不存在的 Key 返回值:" + removedHistoryScore); // 输出:null}
}
2. 进阶操作:修改、清空、判断空否
(1)修改 Value(replace ())
V replace(K key, V value)
:仅当 Key 存在时,用新 Value 替换旧 Value,返回旧 Value(若 Key 不存在则返回 null,区别于put()
:put()
会新增不存在的 Key)。
public class HashMapReplaceDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("英语", 95);// 修改存在的 Key(英语成绩从 95 改为 97)Integer oldEnglishScore = scoreMap.replace("英语", 97);System.out.println("被修改的旧英语成绩:" + oldEnglishScore); // 输出:95System.out.println("修改后:" + scoreMap); // 输出:{数学=98, 英语=97}// 修改不存在的 Key(不会新增,返回 null)Integer oldHistoryScore = scoreMap.replace("历史", 80);System.out.println("修改不存在的 Key 返回值:" + oldHistoryScore); // 输出:nullSystem.out.println("修改后集合:" + scoreMap); // 输出:{数学=98, 英语=97}(无变化)}
}
(2)清空与判断空否(clear ()、isEmpty ()、size ())
void clear()
:清空 HashMap 中所有键值对(集合变为空,对象本身仍存在)。boolean isEmpty()
:判断 HashMap 是否为空(元素个数为 0),返回布尔值。int size()
:返回 HashMap 中键值对的实际个数(区别于 “容量”)。
public class HashMapClearEmptySizeDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("英语", 97);// 1. 获取集合大小System.out.println("初始元素个数:" + scoreMap.size()); // 输出:2// 2. 判断是否为空System.out.println("初始是否为空:" + scoreMap.isEmpty()); // 输出:false// 3. 清空集合scoreMap.clear();System.out.println("清空后元素个数:" + scoreMap.size()); // 输出:0System.out.println("清空后是否为空:" + scoreMap.isEmpty()); // 输出:true}
}
3. HashMap 三种核心遍历方式
HashMap 存储的是 “键值对(Entry)”,遍历需围绕 “Key 集合”“Value 集合”“Entry 集合” 展开,三种常用方式如下:
(1)遍历 Key 集合,再获取 Value(keySet ())
通过 keySet()
获取所有 Key 的集合,遍历 Key 后用 get(key)
获取对应 Value,适合仅需 Key 或需通过 Key 处理 Value 的场景。
public class HashMapKeySetDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("语文", 85);scoreMap.put("英语", 97);// 遍历 Key 集合for (String subject : scoreMap.keySet()) {Integer score = scoreMap.get(subject);System.out.println(subject + ":" + score);}// 输出(顺序不固定):// 数学:98// 语文:85// 英语:97}
}
(2)直接遍历 Entry 集合(entrySet ())
通过 entrySet()
获取所有键值对(Map.Entry<K, V>
)的集合,直接获取 Key 和 Value,效率最高(无需二次 get(key)
查询),是开发首选。
public class HashMapEntrySetDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("语文", 85);scoreMap.put("英语", 97);// 遍历 Entry 集合(推荐)for (HashMap.Entry<String, Integer> entry : scoreMap.entrySet()) {String subject = entry.getKey(); // 获取 KeyInteger score = entry.getValue(); // 获取 ValueSystem.out.println(subject + ":" + score);}// 输出(顺序不固定):// 数学:98// 语文:85// 英语:97}
}
(3)遍历 Value 集合(values ())
通过 values()
获取所有 Value 的集合,仅遍历 Value,适合无需 Key、仅需处理 Value 的场景(无法通过 Value 反向获取 Key)。
public class HashMapValuesDemo {public static void main(String[] args) {HashMap<String, Integer> scoreMap = new HashMap<>();scoreMap.put("数学", 98);scoreMap.put("语文", 85);scoreMap.put("英语", 97);// 遍历 Value 集合System.out.println("所有成绩:");for (Integer score : scoreMap.values()) {System.out.println(score);}// 输出(顺序不固定):// 98// 85// 97}
}
三、HashMap 使用注意事项
Key 的选择原则:
- Key 必须重写
hashCode()
和equals()
方法(否则无法正确判断 Key 唯一性,导致哈希冲突无法解决)。 - 推荐使用不可变类作为 Key(如
String
、Integer
):若 Key 是可变对象,修改后哈希值变化,会导致无法通过原 Key 获取 Value。 - 示例:若用
User
类作为 Key,需手动重写方法:
- Key 必须重写
class User {private String id;// 重写 hashCode() 和 equals()@Overridepublic int hashCode() { return id.hashCode(); }@Overridepublic 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);}
}
null 键值的注意事项:
- Key 最多 1 个 null,重复添加会覆盖;Value 可多个 null。
- 用
get(key)
获取 Value 时,若返回 null,需通过containsKey(key)
确认是 “Key 不存在” 还是 “Value 为 null”。
线程安全问题:
- 单线程环境:直接使用 HashMap 即可。
- 多线程环境:
- 若需弱一致性:使用
Collections.synchronizedMap(new HashMap<>())
(对整个 HashMap 加锁,性能较低)。 - 若需高性能:优先使用
ConcurrentHashMap
(JDK 1.8+ 采用分段锁,性能优于同步 HashMap)。
- 若需弱一致性:使用
性能优化技巧:
- 初始容量指定:若已知元素数量,创建时指定初始容量(如
new HashMap<>(100)
),避免频繁扩容(扩容需 rehash,消耗性能)。 - 负载因子调整:默认 0.75 是 “性能与空间” 的平衡,若内存充足可降低(如 0.5,减少哈希冲突),若内存紧张可提高(如 0.8,减少数组占用空间)。
- 避免哈希冲突:合理重写 Key 的
hashCode()
方法,尽量让哈希值均匀分布,减少链表 / 红黑树的长度。
- 初始容量指定:若已知元素数量,创建时指定初始容量(如
与 TreeMap/Hashtable 的区别(避免混淆):
特性 | HashMap | TreeMap | Hashtable(不推荐) |
---|---|---|---|
排序 | 无序(按哈希值) | 有序(Key 自然排序 / 自定义排序) | 无序 |
线程安全 | 非线程安全 | 非线程安全 | 线程安全(全方法同步) |
null 允许 | Key 1 个 null,Value 多个 null | 不允许 null | 不允许 null |
底层结构 | 数组 + 链表 / 红黑树 | 红黑树 | 数组 + 链表 |
适用场景 | 通用高效查询 | 需要有序键值对 | 遗留多线程场景(已被 ConcurrentHashMap 替代) |
四、总结
HashMap 是 Java 键值对集合的核心实现,核心优势在于 “哈希表” 带来的 O (1) 高效性能,适合大多数无需有序、单线程的键值对存储场景。掌握其底层结构(数组 + 链表 / 红黑树)、常用方法(put/get/remove/ 遍历)及使用注意事项(Key 重写方法、线程安全、性能优化),就能在开发中灵活应对各类场景。