作为一名在 Java 后端开发领域耕耘了八年的工程师,HashMap 可以说是我日常开发中最常用的工具之一。这个看似简单的哈希表实现,实际上蕴含了许多精妙的设计和优化。本文将从原理、源码和实战三个方面,深入剖析 HashMap 的奥秘。
一、HashMap 的底层数据结构
HashMap 的底层数据结构是数组 + 链表 + 红黑树。在 JDK 8 之前,HashMap 的实现是数组 + 链表,而 JDK 8 引入了红黑树来优化链表过长时的性能问题。
核心数据结构代码
java
// HashMap的主干数组,用于存储链表或红黑树的头节点
transient Node<K,V>[] table;
// 链表节点结构
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 哈希值
final K key; // 键
V value; // 值
Node<K,V> next; // 指向下一个节点的引用
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
// 红黑树节点结构(继承自LinkedHashMap.Entry,而LinkedHashMap.Entry继承自Node)
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // 父节点
TreeNode<K,V> left; // 左子节点
TreeNode<K,V> right; // 右子节点
TreeNode<K,V> prev; // 前驱节点
boolean red; // 颜色属性(红或黑)
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// 红黑树相关方法...
}
为什么要引入红黑树?
当链表长度过长时(默认超过 8),链表的查找效率会从 O (1) 退化为 O (n)。而红黑树是一种自平衡的二叉搜索树,它的查找、插入和删除操作的时间复杂度都是 O (log n),可以有效提高 HashMap 在这种情况下的性能。
二、HashMap 的核心原理
1. 哈希函数与哈希冲突
HashMap 的哈希函数设计非常巧妙,目的是将键均匀地分布到数组中,减少哈希冲突的发生。
java
// 计算键的哈希值
static final int hash(Object key) {
int h;
// 将key的hashCode与高16位进行异或运算,提高低位的随机性
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 确定数组索引位置
(n - 1) & hash // n为数组长度
2. 哈希冲突的解决方法
HashMap 采用链地址法来解决哈希冲突,即当两个键的哈希值相同时,它们会被存储在同一个链表或红黑树中。
3. 扩容机制
当 HashMap 中的元素数量超过阈值(容量 * 负载因子)时,就需要进行扩容操作,以保证 HashMap 的性能。
ini
// 扩容方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 处理旧容量不为0的情况
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 阈值翻倍
}
// 处理旧容量为0但旧阈值大于0的情况(使用指定初始容量创建的情况)
else if (oldThr > 0)
newCap = oldThr;
// 处理旧容量和旧阈值都为0的情况(默认初始化)
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 计算新的阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 创建新数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 将旧数组中的元素转移到新数组中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null; // 释放旧数组的引用,帮助GC
if (e.next == null) // 只有一个节点的情况
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode) // 红黑树节点的情况
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // 链表节点的情况
// 这里采用了一种优化策略,将链表分为高位链和低位链
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 判断该节点在新数组中的位置是否发生变化
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 将低位链放入原索引位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 将高位链放入新索引位置(原索引+旧容量)
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
三、HashMap 的实战使用
1. 基本使用示例
typescript
import java.util.HashMap;
import java.util.Map;
public class HashMapExample {
public static void main(String[] args) {
// 创建HashMap实例
Map<String, Integer> scoreMap = new HashMap<>();
// 添加元素
scoreMap.put("Alice", 90);
scoreMap.put("Bob", 85);
scoreMap.put("Charlie", 95);
// 获取元素
Integer aliceScore = scoreMap.get("Alice");
System.out.println("Alice's score: " + aliceScore); // 输出: 90
// 检查键是否存在
boolean containsBob = scoreMap.containsKey("Bob");
System.out.println("Contains Bob? " + containsBob); // 输出: true
// 遍历HashMap
for (Map.Entry<String, Integer> entry : scoreMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 删除元素
scoreMap.remove("Charlie");
System.out.println("After removing Charlie: " + scoreMap);
}
}
2. 性能调优
在使用 HashMap 时,合理设置初始容量和负载因子可以有效提高性能。
javascript
// 创建HashMap时指定初始容量和负载因子
// 预计存储1000个元素,为避免扩容,初始容量设为1000/0.75=1334,向上取最近的2的幂,即2048
Map<String, Object> configMap = new HashMap<>(2048, 0.75f);
3. 线程安全问题
HashMap 是非线程安全的,如果在多线程环境下使用,需要考虑线程安全问题。可以使用 ConcurrentHashMap 替代,或者使用 Collections.synchronizedMap 包装。
javascript
// 使用ConcurrentHashMap保证线程安全
import java.util.concurrent.ConcurrentHashMap;
Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
// 使用Collections.synchronizedMap包装HashMap
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
四、使用 HashMap 的注意事项
1. 键的 equals 和 hashCode 方法
HashMap 使用键的 equals 方法来判断两个键是否相同,使用 hashCode 方法来计算哈希值。因此,作为键的类必须正确重写这两个方法。
java
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 重写equals方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
// 重写hashCode方法
@Override
public int hashCode() {
return Objects.hash(name, age);
}
// Getters and setters...
}
2. 初始容量和负载因子的选择
- 默认初始容量为 16,负载因子为 0.75。
- 如果需要存储大量元素,应适当设置初始容量,避免频繁扩容。
- 负载因子过小会导致空间浪费,过大会增加哈希冲突的概率。
3. 遍历顺序
HashMap 不保证元素的顺序,且顺序可能会随着扩容而改变。如果需要有序的 Map,可以使用 LinkedHashMap 或 TreeMap。
五、总结
HashMap 是 Java 中非常重要的一个数据结构,理解它的底层原理对于写出高效、健壮的代码至关重要。本文从数据结构、核心原理、实战使用和注意事项四个方面对 HashMap 进行了全面剖析,希望能帮助大家更好地掌握和使用 HashMap。
在实际开发中,根据具体场景选择合适的 Map 实现,并合理配置参数,可以充分发挥 HashMap 的性能优势。同时,注意避免常见的陷阱,如键的 equals 和 hashCode 方法的正确实现,以及多线程环境下的线程安全问题。