Java集合Map总结
- [一、Map 核心特性](#一、Map 核心特性)
- [二、Map 接口核心方法](#二、Map 接口核心方法)
- [三、Map 主要实现类](#三、Map 主要实现类)
-
- [1. HashMap(最常用)](#1. HashMap(最常用))
-
- [* 底层结构演变](#* 底层结构演变)
- [* 核心原理:哈希计算与桶定位](#* 核心原理:哈希计算与桶定位)
- [* 关键参数](#* 关键参数)
- [* 扩容机制(resize)](#* 扩容机制(resize))
- [* 使用 HashMap 的关键注意事项](#* 使用 HashMap 的关键注意事项)
- [* HashMap 性能对比](#* HashMap 性能对比)
- [* 总结](#* 总结)
- [2. LinkedHashMap](#2. LinkedHashMap)
- [3. TreeMap](#3. TreeMap)
- [4. Hashtable(过时,不推荐使用)](#4. Hashtable(过时,不推荐使用))
- [5. ConcurrentHashMap](#5. ConcurrentHashMap)
- [四、Map 遍历方式对比](#四、Map 遍历方式对比)
- [五、使用 Map 的注意事项](#五、使用 Map 的注意事项)
-
- 1.键的哈希与相等性:
- 2.避免频繁扩容:
- 3.线程安全问题:
- [4.null 键 / 值处理:](#4.null 键 / 值处理:)
- [5.遍历中修改 Map:](#5.遍历中修改 Map:)
- 六、总结
Map 是 Java 中最核心的集合之一,是 Java 集合框架(java.util 包)中 键值对(Key-Value) 存储的核心接口,是独立的顶级接口。其核心特性是: 键(Key)唯一且不可重复,值(Value)可重复 ,通过键可以快速查找对应的值,是日常开发中最常用的集合之一。
一、Map 核心特性
- 1.键值映射:每个键对应一个值,键是唯一的(重复放入相同键会覆盖原有值),值可重复。
- 2.无序 / 有序:
- 基础实现(如 HashMap)是无序的(不保证插入顺序和遍历顺序一致);
- 有序实现(如 LinkedHashMap、TreeMap)可保证插入顺序或排序顺序。
- 3.允许 null:
- HashMap、LinkedHashMap 允许键和值为 null(但键只能有一个 null);
- Hashtable、TreeMap 不允许键或值为 null。
- 4.非线程安全:大部分实现(HashMap、LinkedHashMap、TreeMap)非线程安全,Hashtable 是线程安全但性能差,推荐使用 Collections.synchronizedMap() 或 ConcurrentHashMap(JUC 包)。
二、Map 接口核心方法
Map 定义了操作键值对的核心方法,所有实现类都需实现这些方法,常用方法如下:
| 方法签名 | 功能说明 |
|---|---|
| V put(K key, V value) | 插入键值对,若键已存在则覆盖原值,返回被覆盖的值(无则返回 null) |
| V get(Object key) | 根据键获取值,键不存在返回 null |
| V remove(Object key) | 删除指定键的键值对,返回被删除的值(无则返回 null) |
| boolean containsKey(Object key) | 判断是否包含指定键 |
| boolean containsValue(Object value) | 判断是否包含指定值(效率低,需遍历所有值) |
| int size() | 返回键值对数量 |
| boolean isEmpty() | 判断是否为空 |
| void clear() | 清空所有键值对 |
| Set keySet() | 返回所有键的 Set 集合(视图,修改会同步到原 Map) |
| Collection values() | 返回所有值的 Collection 集合(视图) |
| Set<Map.Entry<K, V>> entrySet() | 返回所有键值对(Entry 对象)的 Set 集合(遍历推荐用此方法) |
核心内部接口:Map.Entry<K, V>
Map.Entry 是 Map 的内部接口,代表一个键值对,常用方法:
- K getKey():获取键;
- V getValue():获取值;
- V setValue(V value):修改值(会同步到原 Map)。
三、Map 主要实现类
Java 提供了多种 Map 实现,适配不同场景,核心实现类如下:
1. HashMap(最常用)
核心特点
- 底层实现 :JDK 1.8 前是「数组 + 链表」,JDK 1.8 后是「数组 + 链表 + 红黑树」(链表长度 ≥8 且数组长度 ≥64 时,链表转红黑树,提升查询效率)。
- 无序:遍历顺序与插入顺序无关。
- 允许 null 键 / 值:键只能有一个 null,值可多个 null。
- 初始容量与负载因子 :
- 默认初始容量 16,负载因子 0.75(当元素数量 ≥ 容量 × 负载因子时,触发扩容,扩容为原容量 2 倍);
- 容量必须是 2 的幂(哈希桶定位优化)。
- 线程安全 :非线程安全,多线程环境下可能出现死循环(JDK 1.7)、数据丢失等问题。
适用场景
日常开发中绝大多数场景(非线程安全、无需有序),如缓存、配置存储等。
示例代码
java
import java.util.HashMap;
import java.util.Map;
public class HashMapDemo {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
// 插入键值对
map.put("Apple", 10);
map.put("Banana", 20);
map.put("Apple", 15); // 覆盖原有值
// 获取值
System.out.println(map.get("Banana")); // 20
System.out.println(map.get("Apple")); // 15
// 遍历键值对(推荐 entrySet)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}
// 删除
map.remove("Banana");
System.out.println(map.size()); // 1
}
}
* 底层结构演变
HashMap 的底层结构在 JDK 1.8 有重大优化,核心目标是解决哈希冲突导致的链表过长问题,提升查询效率。
| JDK 版本 | 核心结构 | 哈希冲突解决 | 查询时间复杂度 |
|---|---|---|---|
| 1.7 及之前 | 数组(哈希桶)+ 单向链表 | 链地址法(链表存储冲突元素) | O (n)(链表越长效率越低) |
| 1.8 及之后 | 数组(哈希桶)+ 单向链表 + 红黑树 | 链地址法 + 红黑树优化(链表转树) | 平均 O (1),最坏 O (log n) |
核心概念说明
1.哈希桶(Bucket) :数组的每个元素称为哈希桶,存储链表 / 红黑树的头节点,数组下标通过「键的哈希值」计算得到。
2.哈希冲突 :不同键的哈希值计算出相同的数组下标,需通过链表 / 红黑树存储这些冲突元素。
3.红黑树转换条件:
- 链表长度 ≥ 8 且 数组长度 ≥ 64 时,链表转为红黑树;
- 红黑树节点数 ≤ 6 时,转回链表(避免红黑树维护成本过高)。
* 核心原理:哈希计算与桶定位
HashMap 能高效查找的核心是「哈希值计算 + 桶定位」,步骤如下:
1.键的哈希值计算
JDK 1.8 对键的 hashCode() 做了优化,减少哈希冲突:
java
static final int hash(Object key) {
int h;
// 1. 取key的hashCode()
// 2. 高位与低位异或(扰动函数),让高位参与哈希计算,减少冲突
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 若键为 null,哈希值固定为 0(因此 null 键永远存在下标 0 的桶中);
- 「扰动函数」h ^ (h >>> 16):将哈希值的高 16 位与低 16 位异或,让高位特征参与桶定位,降低数组长度较小时的冲突概率。
2.桶下标计算
通过哈希值计算数组下标,确保结果在数组长度范围内:
java
// n 是数组长度,n >= 1(必须是 2 的幂(2⁰,2¹,2²,......))
int index = (n - 1) & hash;
- 为什么用 & 而非 %?因为当 n 是 2 的幂时,(n-1) & hash 等价于 hash % n,但位运算效率远高于取模;
- 要求数组长度必须是 2 的幂(2⁰,2¹,2²,...),是该计算方式的前提(HashMap 初始化 / 扩容时会保证这一点)。
* 关键参数
| 参数名 | 默认值 | 作用 |
|---|---|---|
| initialCapacity(初始容量) | 16 | 哈希桶数组的初始长度,必须是 2 的幂(若手动指定非 2 的幂,会自动调整为最近的 2 的幂) |
| loadFactor(负载因子) | 0.75 | 扩容阈值的计算系数:threshold = capacity × loadFactor |
| threshold(扩容阈值) | 16 × 0.75 = 12 | 当 HashMap 中元素数量 ≥ threshold 时,触发扩容 |
| size | 0 | 实际存储的键值对数量 |
| modCount | 0 | 结构修改次数(用于快速失败机制,遍历中修改会抛 ConcurrentModificationException) |
参数设计的合理性
- 负载因子 0.75:是「时间效率」和「空间效率」的平衡。
- 负载因子过高(如 1.0):空间利用率高,但哈希冲突概率大幅增加,链表 / 红黑树变长,查询变慢;
- 负载因子过低(如 0.5):哈希冲突少,但数组空桶多,空间浪费。
* 扩容机制(resize)
HashMap 的 resize()是核心底层逻辑,目的是解决哈希桶数组容量不足导致的哈希冲突加剧问题,通过扩大数组容量、重新分配元素位置,维持「平均 O (1)」的操作效率。
java
final Node<K,V>[] resize() {
// 1. 保存原哈希桶数组的引用
Node<K,V>[] oldTab = table;
// 2. 初始化 oldCap:原数组为 null(未初始化)则 oldCap=0,否则为原数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 3. 原扩容阈值(辅助计算 newCap)
int oldThr = threshold;
// 4. 声明 newCap(扩容后的容量)、newThr(扩容后的阈值)
int newCap, newThr = 0;
// ===== 分场景计算 newCap =====
// 场景1:原数组已初始化(oldCap > 0,即已完成过首次扩容)
if (oldCap > 0) {
// 1.1 原容量达到最大值(2^30),不再扩容,直接返回原数组
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 1.2 原容量翻倍作为 newCap(oldCap << 1 等价于 oldCap × 2),且不超过最大值
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
newThr = oldThr << 1; // 阈值也翻倍
}
}
// 场景2:原数组未初始化,但阈值已设置(如手动指定初始容量的构造函数)
else if (oldThr > 0) {
newCap = oldThr; // 扩容后的容量 = 原阈值(因为构造时 threshold 已被设为调整后的 2 的幂)
}
// 场景3:原数组未初始化,且阈值为 0(无参构造)
else {
newCap = DEFAULT_INITIAL_CAPACITY; // newCap = 16(默认初始容量)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 阈值=12
}
// ... 后续迁移元素到新数组(容量为 newCap)...
}
HashMap 的哈希桶数组(table)长度固定时,随着元素增加,哈希冲突会越来越严重(链表 / 红黑树变长),查询 / 插入性能下降。扩容通过「翻倍数组容量 + 重新散列元素」,降低冲突概率。
在 HashMap 的 resize() 方法中,oldCap 和 newCap 是描述哈希桶数组容量的核心变量,分别对应「扩容前的数组容量」和「扩容后的数组容量」
| 变量名 | 全称(语义) | 核心含义 | 数据类型 |
|---|---|---|---|
| oldCap | old Capacity | 扩容前哈希桶数组(table)的长度(容量) | int |
| newCap | new Capacity | 扩容后哈希桶数组(table)的目标长度(容量) | int |
| 场景 | oldCap(扩容前) | newCap(扩容后/初始化后) | 说明 |
|---|---|---|---|
| 无参构造首次扩容 | 0(数组未初始化) | 16 | 懒加载,首次 put 触发初始化,容量为默认 16 |
| 第一次扩容(16→32) | 16 | 32 | 容量翻倍(16×2) |
| 第二次扩容(32→64) | 32 | 64 | 容量继续翻倍 |
| 手动指定初始容量 0 | 0 | 1 | tableSizeFor(0) 调整为 1,扩容后容量 1 |
| 手动指定初始容量 1(initialCapacity=1) | 1 | 1 | tableSizeFor(1) 直接返回 1(已是 2 的幂,无需调整),初始化后容量为 1 |
| 容量达最大值(2^30) | 1073741824(2^30) | 不变化 | 不再扩容,返回原数组 |
HashMap 哈希桶数组的最小合法长度是 1(2⁰),由 tableSizeFor方法兜底保障;无参构造的默认长度是 16,而长度 1 仅作为边界场景存在(手动指定容量 0/1 时触发),实际开发中应避免使用(哈希冲突极高)。
HashMap 通过 tableSizeFor(int cap) 方法强制将容量调整为「不小于输入值的最小 2 的幂」,且兜底返回 1,确保长度不会小于 1:
java
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
// 核心兜底:若 n < 0(如 cap=0 时,n=-1),返回 1;否则返回 n+1(保证 ≥1)
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
关键验证:
- 当 cap=0 时:n = 0-1 = -1 → 触发 n < 0,返回 1;
- 当 cap=1 时:n = 0 → 移位后仍为 0 → 返回 0+1=1;
- 当 cap≥2 时:返回最小的 2 的幂(如 cap=3 → 返回 4,cap=5 → 返回 8)。
不同场景下的数组长度初始化:
| 场景 | 手动指定初始容量 | 数组长度调整结果 | 说明 |
|---|---|---|---|
| 场景 1 | new HashMap<>(0) | 1 | tableSizeFor(0) 会返回 1(最小 2 的幂) |
| 场景 2 | new HashMap<>(1) | 1 | 1 本身是 2 的幂,无需调整 |
| 场景 3 | new HashMap<>(2) | 2 | 2 是 2 的幂,直接使用 |
| 场景 4 | new HashMap<>()(无参) | 首次 put 后为 16 | 无参构造默认初始容量是 16(1 << 4) |
| 场景 5 | new HashMap<>(-1) | 抛异常 | 容量不能为负数,校验阶段直接报错 |
扩容是 HashMap 性能的核心影响点,JDK 1.8 对扩容逻辑做了优化:
扩容触发条件
- 当 size ≥ threshold 且 新元素插入的桶不为空时,触发扩容;
- 扩容后数组长度变为原来的 2 倍(保证「数组长度是 2 的幂」(2⁰,2¹,2²,...));
- 新阈值 threshold = 新容量 × loadFactor。
JDK 1.8 扩容核心优化
扩容时需要重新计算所有元素的桶下标,JDK 1.8 利用「数组长度是 2 的幂」的特性,简化了下标计算:
- 原下标:index = hash & (oldCap - 1);
- 新下标:newIndex = hash & (newCap - 1) = hash & (2×oldCap - 1);
- 结论:新下标要么等于原下标,要么等于 原下标 + oldCap(无需重新计算哈希,只需判断哈希值的某一位)。
hash 是 HashMap 为了减少哈希冲突,对键的原始 hashCode() 做「高位扰动」后的最终哈希值,是计算桶下标的核心依据:
- 若键为 null:hash = 0(固定值);
- 若键非 null:hash = 键的 hashCode() ^ (键的 hashCode() >>> 16)(扰动函数)。
HashMap 源码中通过 static final int hash(Object key) 方法生成该值,核心代码:
java
static final int hash(Object key) {
int h;
// 三步核心逻辑:
// 1. 若 key 为 null → h = 0;否则取 key 的原始 hashCode()
// 2. 将 h 的高 16 位右移 16 位(h >>> 16),与原 h 做异或(^)
// 3. 返回最终扰动后的 hash 值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
扩容步骤
1.创建新的**哈希桶数组**,长度为原数组的 2 倍;
2.遍历原数组的每个桶,处理桶中的元素(链表 / 红黑树):
- 若为链表:按规则拆分到新数组的两个桶中(保持原顺序,避免死循环);
- 若为红黑树:拆分后若节点数 ≤ 6,转回链表;
3.替换原数组为新数组,更新容量和阈值。
DK 1.7 扩容的坑(死循环)
JDK 1.7 扩容时,链表采用「头插法」,多线程环境下会导致链表成环,进而引发死循环。JDK 1.8 改为「尾插法」,解决了该问题,但 HashMap 仍非线程安全(仍可能出现数据丢失、覆盖等问题)。
* 使用 HashMap 的关键注意事项
①自定义类作为键必须重写 hashCode () 和 equals ()
- 原因:HashMap 通过 hashCode() 定位桶,通过 equals() 判断键是否相等;
- 若不重写:即使两个对象内容相同,也会被当作不同键,导致哈希冲突或重复插入;
- 重写规则:
- equals() 相等的对象,hashCode() 必须相等;
- hashCode() 相等的对象,equals() 不一定相等(哈希冲突)。
反例(错误)
java
class Person {
String name;
public Person(String name) { this.name = name; }
}
// 两个内容相同的Person会被当作不同键
Map<Person, Integer> map = new HashMap<>();
map.put(new Person("张三"), 1);
map.put(new Person("张三"), 2);
System.out.println(map.size()); // 2(错误,期望1)
正例(正确)
java
class Person {
String name;
public Person(String name) { this.name = name; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
Map<Person, Integer> map = new HashMap<>();
map.put(new Person("张三"), 1);
map.put(new Person("张三"), 2);
System.out.println(map.size()); // 1(正确)
②避免频繁扩容
- 若已知 Map 存储的元素数量,初始化时指定容量:
java
// 预计存储1000个元素,指定容量为 1000 / 0.75 ≈ 1334,向上取2的幂为2048
Map<String, Integer> map = new HashMap<>(2048);
- 避免初始化容量为非 2 的幂(HashMap 会自动调整,但可能不符合预期)。
③慎用 null 键 / 值
- null 键只能有一个,null 值可多个;
- 弊端:get(key) 返回 null 时,无法区分「键不存在」和「值为 null」,建议用 containsKey(key) 先判断。
④遍历中修改 Map 的正确方式
- 禁止在增强 for 循环中直接 remove,会触发 ConcurrentModificationException;
- 正确方式:
java
// 方式1:使用Iterator删除
Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
if (entry.getValue() == 2) {
iterator.remove(); // 安全删除
}
}
// 方式2:Java 8+ removeIf
map.entrySet().removeIf(entry -> entry.getValue() == 2);
⑤红黑树树化的前提
链表转红黑树需同时满足两个条件:
- 链表长度 ≥ 8;
- 数组长度 ≥ 64。
若数组长度 < 64,即使链表长度 ≥8,也只会触发扩容而非树化(优先通过扩容减少冲突)。
* HashMap 性能对比
| 操作 | 平均时间复杂度 | 最坏时间复杂度(JDK 1.7) | 最坏时间复杂度(JDK 1.8) |
|---|---|---|---|
| put | O(1) | O (n)(链表过长) | O (log n)(红黑树) |
| get | O(1) | O (n)(链表过长) | O (log n)(红黑树) |
| remove | O(1) | O (n)(链表过长) | O (log n)(红黑树) |
* 总结
HashMap 是 Java 中最核心的集合实现,其设计体现了「哈希表 + 冲突优化」的经典思路:
1.JDK 1.8 引入红黑树,解决了链表过长导致的性能下降问题;
2.哈希计算和桶定位的优化,保证了平均 O (1) 的操作效率;
3.扩容机制的优化(尾插法、下标简化),解决了死循环并提升了扩容性能;
4.非线程安全,高并发场景需使用 ConcurrentHashMap;
5.自定义键必须重写 hashCode () 和 equals (),否则会导致功能异常。
2. LinkedHashMap
核心特点
- 底层实现 :HashMap + 双向链表(维护插入顺序或访问顺序)。
- 有序:默认保证「插入顺序」,也可通过构造函数指定「访问顺序」(最近访问的元素排在末尾)。
- 性能:略低于 HashMap(额外维护链表),但遍历效率更高。
- 其他 :允许 null 键 / 值,非线程安全。
适用场景
需要保证遍历顺序与插入顺序一致的场景,如:LRU 缓存(通过访问顺序实现)、日志记录等。
示例代码(访问顺序)
java
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapDemo {
public static void main(String[] args) {
// 构造函数:初始容量16,负载因子0.75,访问顺序(true)
Map<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true);
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
map.get("B"); // 访问B,B会被移到末尾
// 遍历:A -> C -> B(访问顺序)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
3. TreeMap
核心特点
- 底层实现:红黑树(一种自平衡的二叉查找树)。
- 有序:按键的「自然顺序」(如 Integer 升序、String 字典序)或「自定义比较器(Comparator)」排序。
- 不允许 null 键(会抛出 NullPointerException),值可以为 null。
- 性能:插入、查询、删除的时间复杂度为 O (log n),低于 HashMap。
- 非线程安全。
适用场景
需要对键进行排序的场景,如:排行榜、按日期排序的日志等。
示例代码(自定义比较器)
java
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
public class TreeMapDemo {
public static void main(String[] args) {
// 自定义比较器:按字符串长度降序
Map<String, Integer> map = new TreeMap<>(new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return Integer.compare(s2.length(), s1.length());
}
});
map.put("Apple", 5);
map.put("Banana", 6);
map.put("Cherry", 6);
// 遍历:Banana/Cherry(长度6) -> Apple(长度5)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
}
4. Hashtable(过时,不推荐使用)
核心特点
- 底层实现:数组 + 链表(JDK 1.8 未优化为红黑树)。
- 线程安全:所有方法加 synchronized 锁,性能极低。
- 不允许 null 键 / 值:会抛出 NullPointerException。
- 初始容量:默认 11,负载因子 0.75,扩容为 2× 原容量 + 1。
替代方案
- 线程安全且高性能:java.util.concurrent.ConcurrentHashMap(JUC 包);
- 简单线程安全:Collections.synchronizedMap(new HashMap<>())。
5. ConcurrentHashMap
核心特点
- 线程安全 :JDK 1.7 用「分段锁(Segment)」,JDK 1.8 用「CAS + synchronized」(只锁当前哈希桶,并发性能大幅提升)。
- 底层实现:同 HashMap(数组 + 链表 + 红黑树)。
- 不允许 null 键 / 值(避免与「键不存在返回 null」混淆)。
- 高并发:支持多线程并发读写,性能远高于 Hashtable。
适用场景
多线程环境下的高并发 Map 操作,如:分布式缓存、并发任务存储等。
四、Map 遍历方式对比
| 遍历方式 | 优点 | 缺点 |
|---|---|---|
| keySet() + get() | 代码简单 | 效率低(需两次哈希查找),遍历 + 修改可能抛异常 |
| entrySet() | 效率高(一次遍历获取键值),推荐使用 | 代码略复杂 |
| forEach()(Java 8+) | 简洁(Lambda 表达式) | 无法在遍历中修改 Map(除了通过 Entry.setValue ()) |
| 迭代器(Iterator) | 支持遍历中删除元素 | 代码繁琐 |
遍历示例(Java 8 forEach)
java
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
// Java 8 Lambda 遍历
map.forEach((key, value) -> System.out.println(key + ": " + value));
五、使用 Map 的注意事项
1.键的哈希与相等性:
- 键的类必须重写 hashCode() 和 equals()(如自定义类作为键),否则会导致重复键或查找失败;
- 示例:若 Person 类未重写 hashCode() 和 equals(),则两个内容相同的 Person 对象会被当作不同键。
2.避免频繁扩容:
- 若已知 Map 大小,初始化时指定容量(如 new HashMap<>(100)),减少扩容次数;
- 负载因子不宜修改(默认 0.75 是时间 / 空间的平衡)。
3.线程安全问题:
- 非线程安全 Map(HashMap/LinkedHashMap)在多线程下修改会导致 ConcurrentModificationException;
- 优先使用 ConcurrentHashMap 而非 Hashtable。
4.null 键 / 值处理:
- 避免依赖 null 值(可改用 Optional 包装),防止空指针;
- TreeMap/ConcurrentHashMap 不允许 null 键,需提前校验。
5.遍历中修改 Map:
- 禁止在增强 for 循环中直接删除元素(抛 ConcurrentModificationException);
- 正确方式:使用 Iterator 的 remove() 方法,或 Java 8+ 的 map.entrySet().removeIf(...)。
六、总结
| 实现类 | 有序性 | 线程安全 | null 键 / 值 | 核心场景 |
|---|---|---|---|---|
| HashMap | 无序 | 否 | 允许 | 日常通用场景 |
| LinkedHashMap | 插入 / 访问顺序 | 否 | 允许 | 需有序、LRU 缓存 |
| TreeMap | 键排序 | 否 | 键不允许 | 排序场景 |
| Hashtable | 无序 | 是(低效) | 不允许 | 过时,不推荐 |
| ConcurrentHashMap | 无序 | 是(高效) | 不允许 | 高并发场景 |
HashMap 是 Java 集合框架中最常用的键值对存储实现,基于哈希表实现,兼顾查询、插入、删除的高效性。
实际开发中,优先选择 HashMap(通用)、LinkedHashMap(有序)、ConcurrentHashMap(高并发),避免使用 Hashtable。