HashMap 的工作原理
桶、哈希与冲突
HashMap 的工作原理 是 CoddyKit 上的免费 Java Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Java Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Java Academy 课程共包含 4 节课。
HashMap 存储什么
HashMap 存储键值对,并为查找、插入和删除提供平均 O(1) 的时间复杂度。
它的内部维护着一个名为数组的 table。数组中的每个位置称为一个桶。
- 键决定条目落入哪个桶。
- 通过键进行查找时,返回的就是值。
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
System.out.println(ages.get("Alice"));
}
}对键进行哈希
调用 put(key, value) 时,映射会调用 key.hashCode() 来获取一个 int。
然后,HashMap 会通过内部函数扩散这些位,使得即使哈希码质量较差,也能将条目分布到各个桶中。
- 最终的数值会通过
hash & (table.length - 1)缩减为桶索引。 - 表的长度始终是 2 的幂,因此这个掩码可以正常工作。
public class Main {
public static void main(String[] args) {
String key = "Alice";
int h = key.hashCode();
int spread = h ^ (h >>> 16);
int index = spread & (16 - 1);
System.out.println("hashCode: " + h);
System.out.println("bucket index: " + index);
}
}桶的实际运作
每个桶可以容纳多个条目。当两个键映射到同一个桶时,就发生了冲突。
冲突是正常且意料之中的情况。HashMap 会将桶中的条目链接起来,以此处理冲突。
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<Integer, String> m = new HashMap<>();
for (int i = 0; i < 5; i++) {
m.put(i, "v" + i);
}
System.out.println(m.size() + " entries stored");
}
}冲突与链接
在 Java 8 之前,所有发生冲突的条目都存放在桶内的单向链表中。
查找时会遍历链表,并调用 equals(),直到找到匹配的键。
- 冲突较少时,实际性能仍接近 O(1)。
- 同一个桶中冲突较多时,该桶的性能会逐渐下降到 O(n)。
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> m = new HashMap<>();
m.put("FB", 1);
m.put("Ea", 2);
System.out.println("FB hash: " + "FB".hashCode());
System.out.println("Ea hash: " + "Ea".hashCode());
System.out.println(m.get("FB") + ", " + m.get("Ea"));
}
}为什么 FB 和 Ea 会冲突
字符串 "FB" 和 "Ea" 在 Java 中具有相同的 hashCode()。这是一个经典的冲突示例。
即使哈希码相同,映射仍会将它们分开存储,因为 equals() 会在桶内区分它们。
public class Main {
public static void main(String[] args) {
System.out.println("FB".hashCode() == "Ea".hashCode());
System.out.println("FB".equals("Ea"));
}
}负载因子
负载因子控制表在扩容前可以达到的满载程度。默认值为 0.75。
- 容量为 16、负载因子为 0.75 时,达到 12 个条目就会触发扩容。
- 较低的负载因子会浪费内存,但能减少冲突。
- 较高的负载因子可以节省内存,但会增加冲突。
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<Integer, Integer> m = new HashMap<>(16, 0.75f);
for (int i = 0; i < 12; i++) m.put(i, i);
System.out.println("Stored " + m.size() + " entries");
}
}扩容表
当条目数量超过 capacity * loadFactor 时,表的大小会翻倍。
每个已有条目都会被重新哈希到新的、更大的表中。这是一项开销较大的操作,因此对于大型映射,提前设置容量很重要。
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
// Pre-size to avoid repeated resizes
Map<Integer, Integer> m = new HashMap<>(1024);
for (int i = 0; i < 800; i++) m.put(i, i * 2);
System.out.println("size = " + m.size());
}
}预设容量以提升性能
如果您大致知道要存储多少个条目,请设置初始容量,以避免反复扩容。
经验法则:初始容量 = expectedSize / 0.75 + 1。
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
int expected = 1000;
int capacity = (int) (expected / 0.75) + 1;
Map<Integer, String> m = new HashMap<>(capacity);
System.out.println("Initial capacity hint: " + capacity);
m.put(1, "ok");
System.out.println(m.get(1));
}
}空键和空值
HashMap 允许一个空键和多个空值。
- 空键始终会进入 0 号桶(其哈希值按 0 处理)。
- 使用
getOrDefault,可以避免无法区分键不存在和键对应空值的情况。
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, String> m = new HashMap<>();
m.put(null, "nullKeyValue");
m.put("a", null);
System.out.println(m.get(null));
System.out.println(m.getOrDefault("missing", "default"));
}
}不保证迭代顺序
HashMap不保证迭代顺序。顺序取决于哈希码和桶的布局。
如果需要可预测的顺序,请使用 LinkedHashMap(插入顺序)或 TreeMap(排序顺序)。
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> m = new HashMap<>();
m.put("one", 1);
m.put("two", 2);
m.put("three", 3);
for (Map.Entry<String, Integer> e : m.entrySet()) {
System.out.println(e.getKey() + "=" + e.getValue());
}
}
}get() 路径概览
一次查找遵循以下步骤:
- 计算
hashCode()并扩散各个位。 - 使用掩码找到桶索引。
- 遍历桶,并使用
equals()比较键。 - 返回匹配的值,否则返回空值。
良好的 hashCode 加上正确的 equals 可以让每一步都保持快速。
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> stock = new HashMap<>();
stock.put("apple", 50);
stock.put("pear", 20);
String key = "apple";
Integer qty = stock.get(key);
System.out.println(key + " -> " + qty);
}
}快速检查
请测试您对 HashMap 如何找到桶的理解。
回顾
您已了解 HashMap 的底层工作原理:
- 键会经过哈希处理并映射到桶。
- 冲突通过在桶中链接条目来处理。
- 负载因子(0.75)会触发容量翻倍和重新哈希。
- 预先设置容量可以避免代价高昂的扩容,而且不保证迭代顺序。
接下来,您将看到为什么仅有 hashCode、没有正确的 equals 还不够。
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> m = new HashMap<>(64);
m.put("recap", 1);
System.out.println("HashMap basics complete: " + m.get("recap"));
}
}常见问题解答
「HashMap 的工作原理」课时是免费的吗?
是的 — 「HashMap 的工作原理」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Java Academy 课程的其余内容,请升级到 CoddyKit PRO。 Java Academy 课程共包含 4 节课。
「HashMap 的工作原理」这节课中我会学到什么?
桶、哈希与冲突 你通过在浏览器中直接运行的动手代码来练习 Java Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Java Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Java Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「HashMap 的工作原理」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Java Academy 课中编写并运行代码吗?
能。每节 Java Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- HashMap 的工作原理
- equals/hashCode 契约
- 实现 hashCode
- 树化与性能