作为 Map 的 Key 的对象,必须正确实现 equals() 和 hashCode() 方法。

下面我们展开详细说明。
为什么 Key 对象需要特殊处理?
Map 的核心是 put(K key, V value) 和 get(Object key) 方法。Map 内部需要根据 Key 来存储和查找 Value,这个过程依赖于两个核心方法:
hashCode():HashMap使用这个方法来确定Key应该存储在哪个“桶”(bucket)中,它是一个快速、不精确的定位。equals(Object obj): 当hashCode()定位到同一个桶后,HashMap会使用equals()方法来精确比较两个Key对象是否真的相同,以避免哈希冲突。
Key 对象没有正确实现这两个方法,Map 的行为就会变得不可预测,最常见的问题就是无法通过 Key 找到对应的 Value。
equals() 和 hashCode() 的“契约”
这两个方法必须遵守以下约定:

- 对称性:
a.equals(b)为true,b.equals(a)也必须为true。 - 自反性:
a.equals(a)必须为true。 - 传递性:
a.equals(b)为true,且b.equals(c)为true,a.equals(c)也必须为true。 - 一致性: 多次调用
a.equals(b)的结果必须一致(前提是对象没有被修改)。 - 非空性:
a.equals(null)必须返回false。
最重要的约定:
如果两个对象根据
equals()方法比较是相等的,那么它们调用hashCode()方法必须返回相同的整数。
反过来的约定不成立:
如果两个对象的
hashCode()相同,它们不一定相等(equals()返回false),这就是所谓的“哈希冲突”(Hash Collision)。
最佳实践:如何为自定义类实现 Key?
假设我们有一个 Person 类,我们想用 Person 对象作为 Map 的 Key。
错误的示例 (不实现或错误实现)
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 省略 getter/setter
// 注意:这里没有重写 equals() 和 hashCode()
}
public class Main {
public static void main(String[] args) {
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);
// p1 和 p2 在逻辑上是同一个人,但它们是不同的对象实例
System.out.println(p1.equals(p2)); // 输出 false (继承自 Object 的 equals)
map.put(p1, "Engineer");
// 尝试用 p2 获取值
// 因为 p1.equals(p2) 是 false,map 认为这是两个不同的 key
System.out.println(map.get(p2)); // 输出 null,而不是期望的 "Engineer"
}
}
问题分析:
p1和p2内容相同,但它们是不同的对象。Person类没有重写equals(),所以使用的是Object类的equals(),它比较的是对象的内存地址(),p1.equals(p2)返回false。HashMap认为这是两个完全不同的Key,get(p2)找不到put(p1)时存入的值。
正确的示例 (IDE 自动生成)
现代 IDE(如 IntelliJ IDEA 或 Eclipse)可以一键生成正确的 equals() 和 hashCode() 方法,这是最推荐的方式。
import java.util.Objects;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Getters and Setters
public String getName() { return name; }
public int getAge() { return age; }
// 1. 重写 equals() 方法
@Override
public boolean equals(Object o) {
// 1. 检查是否是同一个对象
if (this == o) return true;
// 2. 检查是否为 null 或类型是否不同
if (o == null || getClass() != o.getClass()) return false;
// 3. 类型转换
Person person = (Person) o;
// 4. 比较所有关键字段
// 使用 Objects.equals() 可以安全处理 null 值
return age == person.age && Objects.equals(name, person.name);
}
// 2. 重写 hashCode() 方法
@Override
public int hashCode() {
// 使用 Objects.hash() 可以方便地生成基于所有字段的哈希码
// 它会自动处理 null 值,并且计算结果与 equals() 方法保持一致
return Objects.hash(name, age);
}
}
public class Main {
public static void main(String[] args) {
Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Alice", 30);
Person p2 = new Person("Alice", 30);
Person p3 = new Person("Bob", 25);
// 验证 equals()
System.out.println("p1.equals(p2): " + p1.equals(p2)); // 输出 true
System.out.println("p1.equals(p3): " + p1.equals(p3)); // 输出 false
map.put(p1, "Software Engineer");
// 现在可以成功获取到值
System.out.println("Get with p2: " + map.get(p2)); // 输出 "Software Engineer"
// 修改 p3 的字段,使其与 p1 相等
p3.setName("Alice");
p3.setAge(30);
System.out.println("p1.equals(p3) after modification: " + p1.equals(p3)); // 输出 true
System.out.println("Get with modified p3: " + map.get(p3)); // 输出 "Software Engineer"
}
}
关键点:
equals(): 定义了对象的“逻辑相等性”,对于Person类,我们认为name和age相同的两个人就是同一个人。hashCode(): 必须与equals()保持一致,因为p1.equals(p2)是true,p1.hashCode()必须等于p2.hashCode(),这样,HashMap才能将它们定位到同一个桶,并最终通过equals()确认它们是同一个Key。
可变对象作为 Key 的风险
将可变对象作为 Map 的 Key 是非常危险的。
class Person {
// ... (同上,包含 equals 和 hashCode)
}
public class MutableKeyProblem {
public static void main(String[] args) {
Map<Person, String> map = new HashMap<>();
Person p = new Person("Charlie", 40);
map.put(p, "Manager");
System.out.println("Before change: " + map.get(p)); // 输出 "Manager"
// !!!!!!危险的操作:修改了作为 Key 的对象 !!!!!!
p.setAge(41);
// 现在再也找不回值了!
System.out.println("After change: " + map.get(p)); // 输出 null
}
}
问题分析:
put(p, "Manager")时,HashMap计算p的哈希码(基于name="Charlie", age=40),并将"Manager"存储在对应的桶中。p.setAge(41)后,对象p的状态被改变了。get(p)时,HashMap重新计算p的哈希码(基于name="Charlie", age=41),这个新的哈希码指向了一个完全不同的桶。HashMap在新的桶里找不到任何东西,返回null。
最佳实践:
尽量使用不可变对象作为
Map的Key。如果一个对象被用作
Key,那么在它被放入Map之后,其决定equals()和hashCode()结果的字段(所有字段)就不应该再被修改。String类是作为Key的完美选择,因为它就是不可变的。
不同 Map 实现对 Key 的要求
| Map 实现 | 对 Key 的要求 | 特点 |
|---|---|---|
HashMap |
必须正确实现 equals() 和 hashCode() |
基于哈希表,性能最高(O(1)),不保证有序。 |
TreeMap |
Key 对象的类必须实现 Comparable 接口,或者构造时提供一个 Comparator。 |
基于红黑树,Key 会根据自然排序或自定义排序规则进行排序,查询时间复杂度为 O(log n)。 |
LinkedHashMap |
必须正确实现 equals() 和 hashCode() |
继承自 HashMap,同时维护了一个双向链表,可以记录 Key 的插入顺序或访问顺序。 |
Hashtable |
必须正确实现 equals() 和 hashCode() |
HashMap 的线程安全版本,性能较差,不推荐使用。 |
TreeMap 示例
import java.util.TreeMap;
class Person implements Comparable<Person> {
private String name;
private int age;
// ... constructor, getters, equals, hashCode ...
@Override
public int compareTo(Person other) {
// 先按年龄排序,如果年龄相同再按名字排序
int ageCompare = Integer.compare(this.age, other.age);
return ageCompare != 0 ? ageCompare : this.name.compareTo(other.name);
}
}
public class TreeMapExample {
public static void main(String[] args) {
Map<Person, String> map = new TreeMap<>();
map.put(new Person("Alice", 30), "Engineer");
map.put(new Person("Bob", 25), "Designer");
map.put(new Person("Charlie", 30), "Manager");
// 遍历 Map,Key 会按年龄和名字排序
for (Map.Entry<Person, String> entry : map.entrySet()) {
System.out.println(entry.getKey().getName() + " -> " + entry.getValue());
}
// 输出:
// Bob -> Designer
// Alice -> Engineer
// Charlie -> Manager
}
}
- 核心规则: 任何作为
MapKey的自定义对象,都必须同时且正确地重写equals()和hashCode()。 - 契约保证:
equals()相等的对象,hashCode()必须相等。 - 最佳实践: 优先使用不可变对象(如
String, 包装类)作为Key,以避免因对象状态改变而导致Map失效。 - 工具辅助: 使用 IDE 自动生成
equals()和hashCode()方法,确保正确性和效率。 - 不同实现: 注意
HashMap和TreeMap对Key的不同要求(哈希 vs. 排序)。
