HashMap 详解
本章定位:这是集合框架中最重要的一章。深入理解 HashMap 的哈希表结构(数组+链表+红黑树)、扩容机制(2 倍)、树化条件和 put/get 的完整源码流程。HashMap 是 Java 面试的"必考题",飞翔科技几乎所有键值对存储场景都基于它构建。本章由架构师白歌带领新人小崔深入源码,大翔对性能数据尤为关注,孔蓝则负责验证并发安全性。
定义速览表
| 属性 | 说明 |
|---|---|
| 底层结构 | Node<K,V>[] table(数组)+ 链表 + 红黑树(JDK 8) |
| 默认初始容量 | 16(DEFAULT_INITIAL_CAPACITY = 1 << 4) |
| 最大容量 | 2^30(MAXIMUM_CAPACITY = 1 << 30) |
| 默认负载因子 | 0.75(DEFAULT_LOAD_FACTOR) |
| 扩容触发条件 | size > capacity * loadFactor |
| 扩容倍数 | 2 倍(保证容量始终为 2 的幂) |
| 链表 → 红黑树阈值 | 链表长度 ≥ 8 且 数组长度 ≥ 64 |
| 红黑树 → 链表阈值 | 树节点数 ≤ 6(resize 时退化) |
| 允许 null 键 | 是(仅一个,hash 值固定为 0) |
| 允许 null 值 | 是 |
| 线程安全 | 否(多线程扩容可能数据丢失) |
| 遍历顺序 | 不保证(可能随容量变化而改变) |
| 扩容策略 | JDK 8 优化:hash & oldCap 判断迁移,无需重新计算 hash |
| 实现接口 | Map<K,V>, Cloneable, Serializable |
底层结构总览
HashMap 在 JDK 8 中的数据结构是一个复合体 —— 数组作为主干,每个桶(bucket)可以是链表或红黑树。这种设计兼顾了查找效率与空间利用。
树化过程状态机
为什么容量必须是 2 的幂?
核心原因:用位运算替代取模运算来定位桶的下标。
// 传统方式:hash % length(取模,效率低)
int index = hash % length;
// HashMap 方式:hash & (length - 1)(位与,效率高)
// 仅在 length = 2^n 时等价于 hash % length
int index = hash & (length - 1);
示例:
length = 16 (0b10000),length - 1 = 15 (0b01111)
hash = 18 (0b10010)
10010
& 01111
= 00010 → index = 2
hash = 33 (0b100001)
100001
& 001111 (高位用 0 补齐)
= 000001 → index = 1
位与比取模快一个数量级(1 个 CPU 周期 vs 多个),这是 HashMap 高效的关键设计。
核心原理:put 方法源码全流程分析
完整流程图
关键步骤深入
步骤 1:扰动函数 —— hash 值的计算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
为什么需要扰动?
hashCode() 返回 32 位整数,但 HashMap 通过 (n-1) & hash 取低几位(例如容量 16 时取低 4 位)。如果 hashCode 的低位分布不均匀,会导致大量冲突。扰动函数将高 16 位与低 16 位异或,使得高位特征也能影响最终的索引计算。
hashCode = 0001 1101 1010 0011 | 1111 0000 0010 1101
h >>> 16 = 0000 0000 0000 0000 | 0001 1101 1010 0011
XOR = 0001 1101 1010 0011 | 1110 1101 1000 1110 ← 高低位混合
& (16-1) = 1110 → 索引 14
步骤 2:链表插入 —— 尾插法(JDK 8 vs JDK 7)
// JDK 8: 尾插法
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null); // 追加到尾部
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash); // 达到阈值,树化
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break; // 找到相同 key,替换
p = e;
}
// JDK 7: 头插法(有死循环风险)
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e); // 新节点插入头部
size++;
}
JDK 8 改为尾插法的原因:头插法在多线程扩容时可能导致链表成环(死循环)。尾插法虽然在多线程下仍有数据丢失问题,但至少不会形成环形链表。
步骤 3:树化条件 —— 为什么是 8 和 64?
static final int TREEIFY_THRESHOLD = 8; // 链表长度 ≥ 8 触发树化判断
static final int MIN_TREEIFY_CAPACITY = 64; // 数组长度 ≥ 64 才真正树化
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize(); // 数组太小 → 优先扩容而非树化
// ... 真正树化
}
为什么阈值是 8?
根据泊松分布,在负载因子 0.75 且 hashCode 均匀分布的情况下,链表长度达到 8 的概率约为 0.00000006(6 千万分之一),几乎不可能发生。一旦发生,说明 hashCode 分布不均(可能是恶意攻击),此时红黑树的 O(log n) 比链表的 O(n) 更有保障。
| 链表长度 | 出现概率(泊松分布,λ=0.5) |
|---|---|
| 0 | 0.60653066 |
| 1 | 0.30326533 |
| 2 | 0.07581633 |
| 3 | 0.01263606 |
| 4 | 0.00157952 |
| 5 | 0.00015795 |
| 6 | 0.00001316 |
| 7 | 0.00000094 |
| 8 | 0.00000006 |
步骤 4:hashCode + equals 契约
// 正确的重写方式(飞翔科技员工类)
class Employee {
private String id; // 工号,唯一标识
private String name;
@Override
public int hashCode() {
return Objects.hash(id); // 只使用决定唯一性的字段
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee)) return false;
Employee other = (Employee) o;
return Objects.equals(this.id, other.id);
}
}
核心契约:
- 两个对象
equals返回 true,则hashCode必须相等 - 两个对象
hashCode相等,equals不一定返回 true(哈希冲突) - 重写
equals必须同时重写hashCode,否则 HashMap/HashSet 行为异常
扩容机制:resize() 详解
JDK 8 扩容优化:无需重新计算 hash
这是 JDK 8 对扩容的重大优化。不需要重新计算每个 key 的 hash 值,只需要判断 hash & oldCap 是否为 0:
// JDK 8 resize() 中节点迁移的核心代码
Node<K,V> loHead = null, loTail = null; // 保持原索引的链表
Node<K,V> hiHead = null, hiTail = null; // 移动到新索引(index + oldCap)的链表
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
// hash & oldCap == 0 → 索引不变
if (loTail == null) loHead = e;
else loTail.next = e;
loTail = e;
} else {
// hash & oldCap != 0 → 索引变为 index + oldCap
if (hiTail == null) hiHead = e;
else hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
newTab[j] = loHead; // 原位置
newTab[j + oldCap] = hiHead; // 新位置
原理证明:
扩容前:index = hash & (oldCap - 1)(取低 k 位,oldCap = 2^k) 扩容后:index_new = hash & (newCap - 1)(取低 k+1 位,newCap = 2^(k+1))
第 k+1 位要么是 0(索引不变),要么是 1(索引 = 原索引 + oldCap)。而 hash & oldCap 正好取出了第 k+1 位的值!
例如 oldCap = 16 (0b10000):
hash1 = 18 (0b10010) → hash1 & oldCap = 0b10010 & 0b10000 = 0 → 索引不变
hash2 = 33 (0b100001) → hash2 & oldCap = 0b100001 & 0b10000 = 16 → 索引变为 index+16
完整代码示例
示例一:飞翔科技员工信息管理系统
import java.util.*;
/**
* 场景:飞翔科技的人事系统需要根据工号快速查找员工信息。
* 大翔要求系统在 10 万员工规模下,查询响应时间不超过 1ms。
* 白歌选择了 HashMap,小崔负责实现基本功能,孔蓝负责边界测试。
*/
public class FeiXiangEmployeeManager {
// ------ 员工实体(不可变,适合做 key) ------
static class Employee {
private final String id; // 工号(唯一标识)
private final String name;
private final String department;
private final double salary;
public Employee(String id, String name, String department, double salary) {
this.id = id;
this.name = name;
this.department = department;
this.salary = salary;
}
// hashCode 和 equals 只依赖 id —— 工号决定了员工身份
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee)) return false;
Employee employee = (Employee) o;
return Objects.equals(id, employee.id);
}
public String getId() { return id; }
public String getName() { return name; }
@Override
public String toString() {
return String.format("%s | %s | %s | ¥%.0f", id, name, department, salary);
}
}
public static void main(String[] args) {
// ========== 1. 创建 HashMap ==========
HashMap<String, Employee> employeeMap = new HashMap<>(16, 0.75f);
// ========== 2. put:录入员工信息 ==========
employeeMap.put("EMP001", new Employee("EMP001", "大翔", "管理层", 80000));
employeeMap.put("EMP002", new Employee("EMP002", "白歌", "技术部", 35000));
employeeMap.put("EMP003", new Employee("EMP003", "小崔", "技术部", 12000));
employeeMap.put("EMP004", new Employee("EMP004", "孔蓝", "测试部", 15000));
employeeMap.put("EMP005", new Employee("EMP005", "Frank", "市场部", 22000));
employeeMap.put("EMP006", new Employee("EMP006", "黄俪", "人事部", 18000));
System.out.println("=== 飞翔科技员工信息管理系统 ===");
System.out.println("员工总数: " + employeeMap.size());
// ========== 3. get:根据工号查询 ==========
System.out.println("\n--- 工号查询 ---");
System.out.println("EMP001: " + employeeMap.get("EMP001"));
System.out.println("EMP003: " + employeeMap.get("EMP003"));
System.out.println("EMP999(不存在): " + employeeMap.get("EMP999"));
// 安全的获取方式(JDK 8)
Employee defaultEmp = new Employee("UNKNOWN", "未知", "无", 0);
System.out.println("EMP002 安全获取: " + employeeMap.getOrDefault("EMP002", defaultEmp));
System.out.println("EMP999 安全获取: " + employeeMap.getOrDefault("EMP999", defaultEmp));
// ========== 4. containsKey / containsValue ==========
System.out.println("\n--- 存在性判断 ---");
System.out.println("包含工号 EMP001? " + employeeMap.containsKey("EMP001"));
System.out.println("包含工号 EMP999? " + employeeMap.containsKey("EMP999"));
// ========== 5. 遍历方式对比 ==========
System.out.println("\n=== 遍历方式 ===");
// 方式一:entrySet —— 推荐,一次遍历拿到 key 和 value
System.out.println("--- entrySet(推荐) ---");
for (Map.Entry<String, Employee> entry : employeeMap.entrySet()) {
System.out.println(" " + entry.getKey() + " → " + entry.getValue().getName());
}
// 方式二:JDK 8 forEach + Lambda —— 最简洁
System.out.println("\n--- forEach(JDK 8 Lambda) ---");
employeeMap.forEach((id, emp) ->
System.out.println(" " + id + " → " + emp.getName())
);
// 方式三:keySet + get —— 不推荐(每次 get 都是一次查找)
System.out.println("\n--- keySet + get(不推荐,多一次查找) ---");
for (String id : employeeMap.keySet()) {
System.out.println(" " + id + " → " + employeeMap.get(id).getName());
}
// ========== 6. 替换操作 ==========
System.out.println("\n--- 替换操作 ---");
employeeMap.replace("EMP003", new Employee("EMP003", "小崔(已晋升)", "技术部", 20000));
System.out.println("替换后 EMP003: " + employeeMap.get("EMP003").getName());
// 仅当旧值匹配时才替换(replace(K, V, V))
boolean replaced = employeeMap.replace("EMP004",
employeeMap.get("EMP004"),
new Employee("EMP004", "孔蓝(高级)", "测试部", 20000));
System.out.println("条件替换结果: " + replaced);
// ========== 7. 删除 ==========
Employee removed = employeeMap.remove("EMP006");
System.out.println("\n删除 EMP006: " + (removed != null ? removed.getName() : "无"));
System.out.println("删除后员工总数: " + employeeMap.size());
// ========== 8. 统计:技术部员工 ==========
long techCount = employeeMap.values().stream()
.filter(e -> "技术部".equals(e.department))
.count();
System.out.println("\n技术部员工数: " + techCount);
}
}
=== 飞翔科技员工信息管理系统 ===
员工总数: 6
--- 工号查询 ---
EMP001: EMP001 | 大翔 | 管理层 | ¥80000
EMP003: EMP003 | 小崔 | 技术部 | ¥12000
EMP999(不存在): null
EMP002 安全获取: EMP002 | 白歌 | 技术部 | ¥35000
EMP999 安全获取: UNKNOWN | 未知 | 无 | ¥0
--- 存在性判断 ---
包含工号 EMP001? true
包含工号 EMP999? false
=== 遍历方式 ===
--- entrySet(推荐) ---
EMP005 → Frank
EMP006 → 黄俪
EMP003 → 小崔
EMP004 → 孔蓝
EMP001 → 大翔
EMP002 → 白歌
--- forEach(JDK 8 Lambda) ---
EMP005 → Frank
EMP006 → 黄俪
EMP003 → 小崔
EMP004 → 孔蓝
EMP001 → 大翔
EMP002 → 白歌
--- keySet + get(不推荐,多一次查找) ---
EMP005 → Frank
EMP006 → 黄俪
EMP003 → 小崔
EMP004 → 孔蓝
EMP001 → 大翔
EMP002 → 白歌
--- 替换操作 ---
替换后 EMP003: 小崔(已晋升)
条件替换结果: true
删除 EMP006: 黄俪
删除后员工总数: 5
技术部员工数: 2
示例二:HashMap 扩容与树化行为验证
import java.util.*;
/**
* 场景:白歌让小崔验证 HashMap 的树化过程和扩容行为。
* 大翔看到验证结果后,对 JDK 8 的优化表示认可。
*/
class PoorHashKey {
private final int id;
public PoorHashKey(int id) { this.id = id; }
// 故意制造哈希冲突:所有对象返回相同的 hashCode
@Override
public int hashCode() { return 1; } // 全部落入同一桶!
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof PoorHashKey)) return false;
return id == ((PoorHashKey) o).id;
}
@Override
public String toString() { return "Key#" + id; }
}
public class HashMapTreeifyDemo {
public static void main(String[] args) {
// ========== 演示一:链表 → 红黑树 ==========
System.out.println("=== 演示一:链表增长 → 触发树化 ===");
HashMap<PoorHashKey, String> map = new HashMap<>();
for (int i = 1; i <= 12; i++) {
map.put(new PoorHashKey(i), "Value" + i);
if (i == 8) {
System.out.println(" 添加第 " + i + " 个元素(达到 TREEIFY_THRESHOLD = 8)");
}
if (i == 9) {
System.out.println(" 添加第 " + i + " 个元素(容量 >= 64 时链表转为红黑树)");
}
}
System.out.println(" 最终 size: " + map.size());
System.out.println(" 所有元素在同一桶中(hashCode 均为 1)");
// 验证数据完整性
for (int i = 1; i <= 12; i++) {
String value = map.get(new PoorHashKey(i));
System.out.println(" get(Key#" + i + ") = " + value);
}
// ========== 演示二:扩容阈值观察 ==========
System.out.println("\n=== 演示二:负载因子 0.75 下的扩容行为 ===");
HashMap<Integer, String> demo = new HashMap<>(4, 0.75f);
int lastCap = getCapacity(demo);
System.out.println("初始容量: " + lastCap + ",触发扩容阈值: " + (int)(lastCap * 0.75));
for (int i = 0; i < 30; i++) {
demo.put(i, "V" + i);
int currentCap = getCapacity(demo);
if (currentCap != lastCap) {
System.out.println(" size=" + demo.size()
+ " → 扩容: " + lastCap + " → " + currentCap
+ " (threshold=" + (int)(currentCap * 0.75) + ")");
lastCap = currentCap;
}
}
System.out.println("\n最终容量: " + getCapacity(demo) + ",元素数: " + demo.size());
// ========== 演示三:JDK 8 putIfAbsent / computeIfAbsent ==========
System.out.println("\n=== 演示三:JDK 8 新增方法 ===");
HashMap<String, List<String>> deptMap = new HashMap<>();
// computeIfAbsent:键不存在时才计算并插入
deptMap.computeIfAbsent("技术部", k -> new ArrayList<>()).add("白歌");
deptMap.computeIfAbsent("技术部", k -> new ArrayList<>()).add("小崔");
deptMap.computeIfAbsent("测试部", k -> new ArrayList<>()).add("孔蓝");
deptMap.computeIfAbsent("市场部", k -> new ArrayList<>()).add("Frank");
deptMap.forEach((dept, members) ->
System.out.println(" " + dept + ": " + members)
);
// merge:合并值
HashMap<String, Integer> salaryMap = new HashMap<>();
salaryMap.put("技术部", 35000);
salaryMap.merge("技术部", 12000, Integer::sum);
salaryMap.merge("测试部", 15000, Integer::sum);
System.out.println("\n部门工资汇总: " + salaryMap);
}
/** 通过反射获取 HashMap 内部 table 数组的长度 */
private static int getCapacity(HashMap<?, ?> map) {
try {
java.lang.reflect.Field field = HashMap.class.getDeclaredField("table");
field.setAccessible(true);
Object[] table = (Object[]) field.get(map);
return table == null ? 0 : table.length;
} catch (Exception e) {
return -1;
}
}
}
=== 演示一:链表增长 → 触发树化 ===
添加第 8 个元素(达到 TREEIFY_THRESHOLD = 8)
添加第 9 个元素(容量 >= 64 时链表转为红黑树)
最终 size: 12
所有元素在同一桶中(hashCode 均为 1)
get(Key#1) = Value1
get(Key#2) = Value2
get(Key#3) = Value3
get(Key#4) = Value4
get(Key#5) = Value5
get(Key#6) = Value6
get(Key#7) = Value7
get(Key#8) = Value8
get(Key#9) = Value9
get(Key#10) = Value10
get(Key#11) = Value11
get(Key#12) = Value12
=== 演示二:负载因子 0.75 下的扩容行为 ===
初始容量: 4,触发扩容阈值: 3
size=4 → 扩容: 4 → 8 (threshold=6)
size=7 → 扩容: 8 → 16 (threshold=12)
size=13 → 扩容: 16 → 32 (threshold=24)
size=25 → 扩容: 32 → 64 (threshold=48)
最终容量: 64,元素数: 30
=== 演示三:JDK 8 新增方法 ===
技术部: [白歌, 小崔]
测试部: [孔蓝]
市场部: [Frank]
部门工资汇总: {技术部=47000, 测试部=15000}
易错场景
反例一:可变对象作为 key
小崔在员工缓存中踩的经典坑 —— 修改了 key 的 hashCode 后数据丢失:
// ❌ 错误:将可变对象作为 HashMap 的 key,修改后无法找到
class MutableKey {
String id;
MutableKey(String id) { this.id = id; }
@Override
public int hashCode() { return Objects.hash(id); }
@Override
public boolean equals(Object o) {
if (!(o instanceof MutableKey)) return false;
return Objects.equals(id, ((MutableKey) o).id);
}
}
HashMap<MutableKey, String> cache = new HashMap<>();
MutableKey key = new MutableKey("user_001");
cache.put(key, "小崔的信息");
key.id = "user_002"; // 修改了 key 的 hashCode!
System.out.println(cache.get(key)); // null!数据丢失!
// 甚至无法 remove(key),造成"内存泄漏"
纠正:使用 String、Integer 等不可变类作为 key,或确保 key 对象的 hashCode 依赖字段不可变。
// ✅ 正确:使用不可变类作为 key
String key = "user_001";
cache.put(key, "小崔的信息");
// String 不可变,hashCode 永远不会变化
反例二:多线程 put 导致数据覆盖
// ❌ 错误:多线程并发 put 导致数据丢失
HashMap<Integer, String> map = new HashMap<>();
CountDownLatch latch = new CountDownLatch(2);
new Thread(() -> {
for (int i = 0; i < 1000; i++) map.put(i, "A");
latch.countDown();
}).start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) map.put(i, "B");
latch.countDown();
}).start();
latch.await();
System.out.println("期望: 1000, 实际: " + map.size()); // 可能 < 1000 甚至异常
纠正:
// ✅ 方案一:使用 ConcurrentHashMap(推荐)
ConcurrentHashMap<Integer, String> concurrentMap = new ConcurrentHashMap<>();
// 内部使用 CAS + synchronized,线程安全且高效
// ✅ 方案二:使用 Collections.synchronizedMap
Map<Integer, String> syncMap = Collections.synchronizedMap(new HashMap<>());
// 所有操作都有 synchronized 保护,但遍历时需手动加锁
反例三:重写 equals 但不重写 hashCode
// ❌ 错误:只重写了 equals,未重写 hashCode
class BadEmployee {
String id;
String name;
BadEmployee(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof BadEmployee)) return false;
BadEmployee e = (BadEmployee) o;
return Objects.equals(id, e.id);
}
// 未重写 hashCode!继承 Object 的 hashCode(基于内存地址)
}
// 测试
HashMap<BadEmployee, String> map = new HashMap<>();
BadEmployee e1 = new BadEmployee("001", "小崔");
map.put(e1, "小崔的信息");
BadEmployee e2 = new BadEmployee("001", "小崔");
System.out.println(e1.equals(e2)); // true —— equals 认为相等
System.out.println(map.get(e2)); // null —— hashCode 不同,找不到!
System.out.println(e1.hashCode() == e2.hashCode()); // false —— 违反了契约!
纠正:
// ✅ 正确:equals 和 hashCode 保持一致性
class GoodEmployee {
String id;
String name;
GoodEmployee(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof GoodEmployee)) return false;
GoodEmployee e = (GoodEmployee) o;
return Objects.equals(id, e.id);
}
@Override
public int hashCode() {
return Objects.hash(id); // 使用与 equals 相同的字段
}
}
反例四:遍历时删除元素不通过迭代器
// ❌ 错误:直接使用 map.remove 会抛 ConcurrentModificationException
HashMap<String, String> map = new HashMap<>();
map.put("A", "1"); map.put("B", "2"); map.put("C", "3");
for (Map.Entry<String, String> entry : map.entrySet()) {
if ("B".equals(entry.getKey())) {
map.remove(entry.getKey()); // ConcurrentModificationException!
}
}
纠正:
// ✅ 使用迭代器的 remove 方法
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
if ("B".equals(entry.getKey())) {
it.remove(); // 安全删除
}
}
// ✅ JDK 8 更简洁的方式
map.entrySet().removeIf(entry -> "B".equals(entry.getKey()));
面试考点
Q1:HashMap 的底层数据结构是怎样的?JDK 7 和 JDK 8 有什么区别?
HashMap 在 JDK 8 中使用数组 + 链表 + 红黑树的复合结构。数组是主干,每个桶可以存链表或红黑树。JDK 7 仅使用数组 + 链表。主要区别: ① 树化:JDK 8 链表长度 ≥ 8 且数组长度 ≥ 64 时转换为红黑树(最坏 O(n) → O(log n)); ② 插入方式:JDK 7 头插法 → JDK 8 尾插法(解决多线程扩容死循环); ③ hash 扰动:JDK 7 扰动 4 次 → JDK 8 扰动 1 次(效率与效果的平衡); ④ 扩容迁移:JDK 7 需要重新计算每个 key 的 hash → JDK 8 通过
hash & oldCap判断,无需重算。
Q2:HashMap 的 put 方法完整流程是怎样的?
① 调用
hash(Object key)计算 hash 值:(h = key.hashCode()) ^ (h >>> 16); ② 判断 table 是否为 null 或 length 为 0,若是则resize()初始化(默认容量 16); ③ 计算桶下标:index = (n - 1) & hash; ④ 若桶为空,直接放入新 Node; ⑤ 若桶中首个节点是 TreeNode,走红黑树插入逻辑putTreeVal(); ⑥ 否则遍历链表:找到相同 key 则替换 value;遍历到尾节点则尾插法追加;追加后若链表长度 ≥ 8,触发treeifyBin(); ⑦treeifyBin()中:若数组长度 < 64 则优先扩容;否则链表转红黑树; ⑧ 插入后size++,若size > threshold(capacity * loadFactor),触发resize()扩容。
Q3:为什么 HashMap 的容量必须是 2 的幂?负载因子为什么是 0.75?
容量为 2 的幂:为了用
hash & (length - 1)替代hash % length。取模运算需要 CPU 做除法(数十个周期),而位与运算仅需 1 个 CPU 周期。但该等价关系仅在 length 为 2 的幂时成立。此外,扩容时通过hash & oldCap就能确定新位置,无需重新计算 hash。负载因子 0.75:这是时间与空间的工程权衡。负载因子越大,空间利用率越高(桶塞得越满),但哈希冲突概率越大(查找退化);负载因子越小,查找越快但空间浪费越大。0.75 在泊松分布下,桶中元素数量分布最为理想(链表长度 ≥ 8 的概率仅为千万分之六)。
Q4:HashMap 为什么线程不安全?具体表现有哪些?如何在并发场景下正确使用?
线程不安全的 4 种表现: ① JDK 7 扩容死循环:头插法 resize 时链表可能成环,get() 进入死循环 CPU 100%(JDK 8 尾插法修复); ② 数据覆盖:两个线程同时 put 到同一桶,后一个覆盖前一个(put 非原子操作); ③ size 计数错误:
size++非原子操作,多线程自增导致计数不准; ④ 扩容时数据丢失:线程 A 迁移节点时,线程 B 将节点放入已迁移的桶,导致节点丢失。并发方案: ① ConcurrentHashMap(首选):JDK 8 使用 CAS + synchronized,锁粒度桶级别,高并发性能最优; ② Collections.synchronizedMap:方法级 synchronized,性能较低但实现简单; ③ Hashtable:遗留类(JDK 1.0),不推荐新代码使用。
Q5:HashMap 与 Hashtable 的区别?
维度 HashMap Hashtable 线程安全 否 是(synchronized 方法) null 键/值 允许各一个 不允许 父类 AbstractMap Dictionary(遗留) 迭代器 fail-fast Iterator fail-fast Iterator + Enumeration 出现时间 JDK 1.2 JDK 1.0 默认容量 16 11 扩容 2 倍 2 倍 + 1 效率 高(无锁) 低(方法级锁)
Q6:HashMap 的 keySet / values / entrySet 返回的是独立副本吗?
都不是独立副本,而是视图(view)。对视图的修改会直接影响 HashMap,反之亦然。例如通过
keySet().remove(key)删除键,会同时删除 HashMap 中的对应条目。如果需要在遍历时安全修改,应使用迭代器的remove()方法或 JDK 8 的removeIf()。这三个视图的迭代器都是 fail-fast 的——在迭代过程中若 HashMap 发生结构性修改(非迭代器自身操作),会抛出ConcurrentModificationException。