equals/hashCode 契约
为什么二者必须一致
equals/hashCode 契约 是 CoddyKit 上的免费 Java Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Java Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Java Academy 课程共包含 4 节课。
两个方法,一个契约
每个 Java 对象都会从 Object 继承 equals() 和 hashCode()。
重写其中一个方法时,几乎总是必须重写另一个方法。它们构成了基于哈希的集合所依赖的严格契约。
public class Main {
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
System.out.println(a.equals(b));
System.out.println(a.hashCode() == b.hashCode());
}
}核心规则
契约规定:
- 如果
a.equals(b)为真,那么a.hashCode()必须等于b.hashCode()。 - 如果哈希码不同,则可以确定对象不相等。
相等的对象必须具有相等的哈希码。反之则不必成立。
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hel" + "lo";
System.out.println(s1.equals(s2));
System.out.println(s1.hashCode() == s2.hashCode());
}
}不相等的对象可以共享哈希值
两个不相等的对象可以共享同一个哈希码。这只是一次冲突,映射会使用 equals() 处理它。
因此,唯一被禁止的情况是:相等的对象具有不同的哈希码。
public class Main {
public static void main(String[] args) {
// Equal value, unequal objects, same hash is fine
System.out.println("FB".hashCode() == "Ea".hashCode());
System.out.println("FB".equals("Ea"));
}
}只重写 equals 的类
这是一个有问题的类。它重写了 equals(),但没有重写 hashCode()。
现在,逻辑上相等的两个点拥有从 Object 继承而来的不同哈希码,违反了契约。
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
// BUG: no hashCode override
}
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println("equals: " + a.equals(b));
System.out.println("same hash: " + (a.hashCode() == b.hashCode()));
}
}HashSet 中的错误
这个有问题的类在 HashSet 中会失败。集合会在错误哈希值选定的桶中查找,因此永远找不到相等的元素。
最终会出现本应被拒绝的重复项。
import java.util.HashSet;
import java.util.Set;
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}修复它
重写这两个方法,并根据同一组字段推导它们。这样相等的对象会共享一个哈希码,并落入同一个桶。
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}equals 必须具有自反性
正确的 equals() 遵守四个性质。第一条是自反性:x.equals(x) 必须为真。
这听起来显而易见,但如果忘记处理同一引用的情况,自定义逻辑就可能破坏这一性质。
public class Main {
public static void main(String[] args) {
String x = "java";
System.out.println(x.equals(x));
}
}对称性与传递性
还有两条规则:
- 对称:如果
a.equals(b),那么b.equals(a)。 - 传递:如果
a.equals(b)且b.equals(c),那么a.equals(c)。
在类层次结构中混用类型经常会破坏对称性,因此请仔细比较类。
public class Main {
public static void main(String[] args) {
String a = "x", b = "x", c = "x";
boolean symmetric = a.equals(b) == b.equals(a);
boolean transitive = a.equals(b) && b.equals(c) && a.equals(c);
System.out.println("symmetric: " + symmetric);
System.out.println("transitive: " + transitive);
}
}一致
第四条规则是一致:只要对象没有发生变化,重复调用就会返回相同结果。
这就是为什么您应当根据不可变字段实现 equals 和 hashCode。在插入后修改 hashCode 使用的字段会破坏映射。
import java.util.Objects;
public class Main {
public static void main(String[] args) {
// Immutable record gives consistent equals/hashCode automatically
record Id(int value) {}
Id id = new Id(7);
System.out.println(Objects.equals(id, new Id(7)));
System.out.println(id.hashCode() == new Id(7).hashCode());
}
}记录类会替您完成这些工作
Java record 会根据其所有组件自动生成 equals() 和 hashCode()。
对于值类型数据,优先使用记录类。这样无需额外工作即可满足契约,并且始终保持同步。
import java.util.HashSet;
import java.util.Set;
public class Main {
record Point(int x, int y) {}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}可变键陷阱
如果将对象放入映射后,修改了 hashCode 使用的字段,您将再也找不到它。
映射会在旧桶中查找;对象现在会哈希到新桶。该条目变成了一个丢失的幽灵。
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Main {
static class Box {
int id;
Box(int id) { this.id = id; }
@Override public boolean equals(Object o) {
return o instanceof Box b && b.id == id;
}
@Override public int hashCode() { return Objects.hash(id); }
}
public static void main(String[] args) {
Map<Box, String> m = new HashMap<>();
Box key = new Box(1);
m.put(key, "value");
key.id = 99; // mutated after insertion
System.out.println(m.get(key));
}
}快速检查
请测试您对这一契约的掌握程度。
回顾
您已了解 equals/hashCode 契约:
- 相等的对象必须具有相等的哈希码。
- equals 必须具有自反性、对称性、传递性和一致性。
- 两个方法都应基于同一组不可变字段。
- 记录类可以免费生成正确的实现。
接下来,您将亲手编写正确的 hashCode。
import java.util.Objects;
public class Main {
public static void main(String[] args) {
System.out.println("Contract recap: " + Objects.hash(1, 2, 3));
}
}常见问题解答
「equals/hashCode 契约」课时是免费的吗?
是的 — 「equals/hashCode 契约」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Java Academy 课程的其余内容,请升级到 CoddyKit PRO。 Java Academy 课程共包含 4 节课。
「equals/hashCode 契约」这节课中我会学到什么?
为什么二者必须一致 你通过在浏览器中直接运行的动手代码来练习 Java Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Java Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Java Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「equals/hashCode 契约」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Java Academy 课中编写并运行代码吗?
能。每节 Java Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- HashMap 的工作原理
- equals/hashCode 契约
- 实现 hashCode
- 树化与性能