1. equals、hashCode、toString 的自动生成
为什么需要这些方法?
在 Java 中处理对象时,你很快就会遇到一些重复性的任务。有时需要检查两个对象是否相等,例如判断它是否已经存在于类似于 Set 或 Map 的集合中。还有些场景对象会被用作 HashMap 的键,没有专门的比较规则就无法正常工作。并且几乎总是希望把对象打印到日志或屏幕上时,不是类似 MyClass@7b23ec81 的一串“乱码”,而是有意义的输出。
为此,Java 中的每个类都有三个特殊方法:
- equals(Object o) 负责判断相等性。
- hashCode() 为对象提供数值“指纹”,供哈希类集合使用。
- toString() 返回对象的可读字符串表示,便于调试与打印。
为什么在普通类里很麻烦?
在普通类中,这些方法需要手写。这时就开始枯燥且头疼了:会产生大量样板代码,只会让类变得臃肿;非常容易出错——比如忘记比较某个字段、错误地计算 hashCode,随后还要排查各种诡异的 Bug。而一旦给类新增字段,就不得不回到这些方法里把所有逻辑再改一遍。
普通类示例
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return 31 * x + y;
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
看起来很眼熟吧?没错,这还只是两个字段!要是有二十个呢?
record 是怎么做的
record 类会替你完成这一切。只需声明:
public record Point(int x, int y) { }
然后 Java 会自动生成:
- 构造器
- 访问器(x()、y())
- equals、hashCode、toString
自动生成的方法
- equals 按值比较 record 的所有组件。
- hashCode 基于所有组件计算。
- toString 返回形如 Point[x=1, y=2] 的字符串。
我们来实际看看!
public record Point(int x, int y) {}
public class Demo {
public static void main(String[] args) {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // true
System.out.println(p1); // Point[x=1, y=2]
}
}
输出:
true
true
Point[x=1, y=2]
一切按预期工作,且无需多写一行样板代码!
2. 为什么这很重要:集合、调试与健壮性
在集合中的正确行为
设想你把对象当作 HashMap 的键或 HashSet 的元素。如果 equals 与 hashCode 实现不正确,集合会表现得很奇怪:要么找不到你刚放进去的元素,要么把两个不同的对象当成相同的。
使用 record 类时你可以放心:比较与哈希总是考虑 record 的所有组件(按它们声明的顺序)。
示例:将 record 用作键
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
record Point(int x, int y) {}
Map<Point, String> map = new HashMap<>();
Point p1 = new Point(3, 4);
map.put(p1, "Hello!");
Point p2 = new Point(3, 4);
System.out.println(map.get(p2)); // "Hello!" — 正常工作!
}
}
请注意:p1 和 p2 是不同的对象(不同引用),但字段值相同,因此被视为相等。关于 Map 和 HashMap 的更多内容,你将在第 26 级学到 :P
便于调试与日志记录
不再是乏味的 Point@1a2b3c4d(普通类的默认打印),record 类会打印得既美观又信息充分:
Point[x=3, y=4]
这能在调试与日志记录时大幅节省时间。
3. record 中 equals、hashCode、toString 的工作方式
equals 方法
record 类实现的 equals 会使两个对象在以下条件下被认为相等:
- 它们是同一类型(同一个 record 类)
- 它们的所有组件都相等(== 用于原始类型,equals() 用于对象)
比较示例
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = new Point(1, 3);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.equals(p3)); // false
hashCode 方法
哈希码基于 record 的所有组件计算,通常通过标准方法 Objects.hash(...) 完成。
System.out.println(p1.hashCode()); // 例如,994
System.out.println(p2.hashCode()); // 也是 994
System.out.println(p3.hashCode()); // 不同的数字
toString 方法
字符串表示始终为以下格式:
ClassName[field1=value1, field2=value2, ...]
System.out.println(p1); // Point[x=1, y=2]
4. 重写 equals、hashCode、toString:何时以及如何?
有时(虽不常见)需要更改这些方法的默认行为。例如,你希望 toString 返回另一种格式,或者只按部分字段进行比较。
注意:如果你要重写 equals/hashCode,请非常谨慎!破坏它们的“契约”可能导致很难排查的 Bug。
如何重写方法
只需在 record 类体内声明你自己的方法:
public record Point(int x, int y) {
@Override
public String toString() {
return "(" + x + "; " + y + ")";
}
}
Point p = new Point(3, 5);
System.out.println(p); // (3; 5)
能否重写 equals/hashCode?
可以,但如果你不够确定自己在做什么,强烈不推荐。例如,你想只按字段 x 比较(这本身就很奇怪):
public record Point(int x, int y) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x;
}
@Override
public int hashCode() {
return Integer.hashCode(x);
}
}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 999);
System.out.println(p1.equals(p2)); // true (!)
但请注意:一旦你重写了 equals,就务必同时重写 hashCode——否则集合会工作不正确。
最佳实践
- 如果不确定为什么要重写——就不要重写!
- 对于 toString——若想自定义格式,可以放心重写。
- 对于 equals/hashCode——只有在确有充分理由且了解其后果时才重写。
5. 实战:对象比较与在集合中使用 record
示例:比较两个 record 对象
public record User(String name, int age) {}
public class Demo {
public static void main(String[] args) {
User u1 = new User("Alice", 20);
User u2 = new User("Alice", 20);
User u3 = new User("Bob", 25);
System.out.println(u1.equals(u2)); // true
System.out.println(u1.equals(u3)); // false
System.out.println(u1.hashCode() == u2.hashCode()); // true
System.out.println(u1); // User[name=Alice, age=20]
}
}
示例:在 HashMap 中把 record 作为键
假设我们有一个应用,按用户名和年龄统计访问次数(以防万一,俱乐部里可能会有两个“Ivan 20 岁”)。
import java.util.HashMap;
import java.util.Map;
public class Demo {
public static void main(String[] args) {
record User(String name, int age) {}
Map<User, Integer> visits = new HashMap<>();
User ivan20 = new User("Ivan", 20);
User ivan22 = new User("Ivan", 22);
visits.put(ivan20, 5);
visits.put(ivan22, 2);
// 验证按值查找是否工作正常
System.out.println(visits.get(new User("Ivan", 20))); // 5
System.out.println(visits.get(new User("Ivan", 22))); // 2
}
}
如果 equals 和 hashCode 没有正确实现,查找就不会成功。关于 Map 和 HashMap 的更多内容,请参见第 26 级的课程 :P
6. 使用 record 类中的 equals、hashCode、toString 时的常见错误
错误 1:以为字段在创建后还能修改。
record 的字段始终是 final,并按构造器设置的值进行比较。如果你通过某些“技巧”去“改变”内部状态(例如字段里持有可变对象且其内容被修改),比较与哈希可能会变得不正确。
错误 2:重写了 equals,却忘了 hashCode。
如果你重写了这两个方法之一——一定要把另一个也一起重写!否则集合(HashSet、HashMap)会表现得不可预测。
错误 3:期待 toString 采用其他格式。
如果你需要特殊的字符串格式——直接重写 toString 即可。默认格式始终是 ClassName[field1=value1, field2=value2]。
错误 4:把 record 用于包含可变字段的复杂类。
record 的字段应该是不可变的。如果字段是例如 ArrayList 之类的可变类型,且有人修改了其内容——比较与哈希码可能会“失效”。对 record 而言,最好只使用不可变类型。
错误 5:将 record 用于并非值对象(value object)的类。
record 不是“语法更短的小类”。它就是值对象(value object),用于保存一组值。如果你的类包含复杂逻辑、可变状态或需要继承——请使用普通类。
GO TO FULL VERSION