当您重写 equals() 以定义逻辑相等性时,您必须也重写 hashCode() — 因为基于哈希的集合(HashMap、HashSet)依赖于相等对象具有相等哈希码的约定。破坏这个约定会导致难以发现的隐蔽错误。
// by default, equals() compares IDENTITY (same object?), hashCode() is based on memory address
Person p1 = new Person("Ann", 30);
Person p2 = new Person("Ann", 30);
p1.equals(p2); // false by default — different objects, even with same data
不进行重写时,具有相同内容的两个对象是"不相等的" — 这通常不是您对值对象想要的行为。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person p = (Person) o;
return age == p.age && Objects.equals(name, p.name); // compare by CONTENT
}
@Override
public int hashCode() {
return Objects.hash(name, age); // MUST be consistent with equals()
}
约定: 如果 a.equals(b) 为真,则 a.hashCode() == b.hashCode() 必须为真。
Person p1 = new Person("Ann", 30);
Person p2 = new Person("Ann", 30); // equal by our equals()
Set<Person> set = new HashSet<>();
set.add(p1);
set.contains(p2); // ❌ likely FALSE — even though p1.equals(p2)!
为什么?HashMap/HashSet 首先使用 hashCode() 找到正确的桶,然后在其中使用 equals()。使用默认的(基于身份的)hashCode() 时,p1 和 p2 会落在不同的桶中,所以 contains 根本不会比较它们 — 集合认为它们是不同的。这会产生令人困惑的错误:Set 中出现重复项、map 查找失败等。
1. equal objects → equal hash codes (REQUIRED for correctness)
2. unequal objects MAY have the same hash (collisions are allowed)
3. hashCode() must be consistent (same object → same code, unchanged)
4. equals() must be reflexive, symmetric, transitive, consistent
record Person(String name, int age) {} // records auto-generate equals/hashCode/toString!
Java records(以及 IDE 生成 / Lombok)会为您生成正确、一致的 equals/hashCode。
equals/hashCode 约定是 Java 最重要且最常被违反的规则之一。
仅重写 equals() 而不重写 hashCode() 会以难以调试的方式破坏基于哈希的集合:您认为相等的对象会在 HashMap/HashSet 中"丢失"(查找失败、幻影重复),因为它们哈希到不同的桶中。
由于这些集合无处不在,每个用作键或集合元素的值类型类都必须同时一致地重写两者。
理解为什么(桶-然后-equals 查找机制)— 以及使用 records 或生成的代码来正确实现 — 对于正确的行为和作为揭示对 Java 集合工作原理真实理解的经典面试题目至关重要。