深入理解 HashMap的数据结构 - 教程
目录
HashMap 是 Java 集合框架中最重要且最常用的数据结构之一。它提供了高效的键值对存储和检索能力,理解 HashMap 的工作原理对于编写高性能的 Java 程序至关重要。
1. HashMap 概述
1.1 什么是 HashMap?
HashMap 是基于哈希表的 Map 接口实现,它存储键值对(key-value pairs),允许 null 键和 null 值,并且不保证元素的顺序。
// 基本使用示例
Map map = new HashMap<>();
map.put("Alice", 25);
map.put("Bob", 30);
map.put("Charlie", 28);
System.out.println(map.get("Alice")); // 输出: 25
1.2 核心特性
- 键唯一性:不允许重复的键
- 允许 null:可以有一个 null 键和多个 null 值
- 非线程安全:多线程环境下需要外部同步
- 快速访问:平均时间复杂度 O(1)
- 无序:不保证元素的顺序
2. 底层实现原理
2.1 数据结构演进
Java 7 及之前:数组 + 链表
// Java 7 的HashMap结构
public class HashMap {
transient Entry[] table; // 数组
static class Entry implements Map.Entry {
final K key;
V value;
Entry next; // 链表指针
int hash;
}
}
Java 8 及之后:数组 + 链表/红黑树
// Java 8 的HashMap结构
public class HashMap {
transient Node[] table; // 数组
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next; // 链表指针
}
static final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; // 红黑树节点
TreeNode left;
TreeNode right;
TreeNode prev; // 链表指针
boolean red;
}
}
2.2 哈希函数
HashMap 使用 hashCode() 方法计算键的哈希值:
// HashMap 中的哈希函数 (Java 8)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
哈希优化:通过异或高16位和低16位,减少哈希冲突。
3. 核心工作机制
3.1 put() 方法流程
// put 方法简化流程
public V put(K key, V value) {
// 1. 计算哈希值
int hash = hash(key);
// 2. 计算数组索引
int index = (table.length - 1) & hash;
// 3. 处理哈希冲突
if (table[index] == null) {
// 直接创建新节点
table[index] = newNode(hash, key, value, null);
} else {
// 处理已存在节点(链表或红黑树)
handleExistingNode(table[index], hash, key, value);
}
// 4. 检查扩容
if (++size > threshold) {
resize();
}
return null;
}
3.2 get() 方法流程
// get 方法简化流程
public V get(Object key) {
// 1. 计算哈希值
int hash = hash(key);
// 2. 计算数组索引
int index = (table.length - 1) & hash;
// 3. 在链表/红黑树中查找
Node first = table[index];
if (first instanceof TreeNode) {
// 红黑树查找
return ((TreeNode)first).getTreeNode(hash, key);
} else {
// 链表查找
Node e = first;
while (e != null) {
if (e.hash == hash &&
(e.key == key || (key != null && key.equals(e.key)))) {
return e.value;
}
e = e.next;
}
}
return null;
}
4. 扩容机制
4.1 扩容条件
HashMap 在以下情况下会进行扩容:
- 元素数量超过负载因子(load factor)* 当前容量
- 默认负载因子为 0.75
// 扩容阈值计算
int threshold = capacity * loadFactor;
// 默认值
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
4.2 扩容过程
// resize() 方法简化流程
final Node[] resize() {
// 1. 计算新容量(通常是原来的2倍)
int newCap = oldCap [] newTab = new Node[newCap];
// 3. 重新哈希所有元素
for (Node e : oldTab) {
while (e != null) {
Node next = e.next;
int newIndex = (newCap - 1) & e.hash;
e.next = newTab[newIndex];
newTab[newIndex] = e;
e = next;
}
}
return newTab;
}
5. 树化与反树化
5.1 树化条件(链表 → 红黑树)
// 树化阈值
static final int TREEIFY_THRESHOLD = 8;
// 最小树化容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 当链表长度 >= 8 且数组容量 >= 64 时树化
if (binCount >= TREEIFY_THRESHOLD - 1) {
treeifyBin(tab, hash);
}
5.2 反树化条件(红黑树 → 链表)
// 反树化阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 当红黑树节点数 <= 6 时退化为链表
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
}
6. 性能分析
6.1 时间复杂度
操作 | 平均情况 | 最坏情况 |
put() | O(1) | O(log n) |
get() | O(1) | O(log n) |
remove() | O(1) | O(log n) |
containsKey() | O(1) | O(log n) |
6.2 影响性能的因素
- 哈希函数质量:好的哈希函数减少冲突
- 负载因子:影响扩容频率
- 初始容量:避免频繁扩容
- 键的分布:均匀分布减少冲突
7. 线程安全问题
7.1 为什么 HashMap 非线程安全?
// 多线程下的问题示例
public class HashMapThreadSafeTest {
public static void main(String[] args) throws InterruptedException {
Map map = new HashMap<>();
Thread t1 = new Thread(() -> {
for (int i = 0; i {
for (int i = 0; i < 1000; i++) {
map.put("key" + i, i * 2);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
// 可能产生各种问题:数据丢失、死循环(Java7)、数据不一致
}
}
7.2 线程安全替代方案
// 1. Collections.synchronizedMap
Map syncMap =
Collections.synchronizedMap(new HashMap<>());
// 2. ConcurrentHashMap (推荐)
Map concurrentMap = new ConcurrentHashMap<>();
// 3. Hashtable (已过时)
Map hashtable = new Hashtable<>();
8. 最佳实践
8.1 正确使用 HashMap
// 好的实践
public class HashMapBestPractices {
// 1. 指定初始容量和负载因子
Map map = new HashMap<>(100, 0.8f);
// 2. 使用合适的键类型
public void testGoodKeys() {
// String、Integer 等不可变类作为键
Map userMap = new HashMap<>();
Map productMap = new HashMap<>();
}
// 3. 避免在迭代时修改
public void safeIteration() {
Map map = new HashMap<>();
// 填充数据...
// 使用迭代器安全删除
Iterator> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = it.next();
if (shouldRemove(entry)) {
it.remove(); // 安全删除
}
}
}
}
8.2 自定义对象作为键
// 自定义键类必须正确实现 hashCode() 和 equals()
public class Employee {
private final String id;
private final String name;
public Employee(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return Objects.equals(id, employee.id) &&
Objects.equals(name, employee.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
}
// 使用自定义对象作为键
Map employeeDepartmentMap = new HashMap<>();
9. 常见面试问题
9.1 HashMap 的工作原理?
HashMap 基于哈希表实现,通过键的 hashCode() 计算存储位置,使用 equals() 解决哈希冲突。
9.2 为什么使用红黑树?
当链表长度过长时,查找性能从 O(n) 降为 O(log n),提高最坏情况下的性能。
9.3 负载因子为什么是 0.75?
在时间和空间成本上做了折衷,0.75 提供了较好的性能平衡。
9.4 为什么容量是 2 的幂?
方便使用位运算计算索引:index = (n - 1) & hash,比取模运算更高效。
10. 总结
HashMap 的核心要点:
- 数据结构:数组 + 链表/红黑树(Java 8+)
- 哈希函数:通过高位异或减少冲突
- 扩容机制:2倍扩容,重新哈希
- 树化机制:链表长度 >= 8 且容量 >= 64 时树化
- 线程安全:非线程安全,需要外部同步
使用建议:
- 预估容量:避免频繁扩容
- 选择合适的键:使用不可变对象
- 重写 hashCode() 和 equals():自定义键对象时
- 多线程环境:使用 ConcurrentHashMap
- 迭代修改:使用迭代器的 remove() 方法
浙公网安备 33010602011771号