讲解
Java 里所有类都直接或间接继承 Object,因此每个对象天生带着几个方法:toString() 返回对象的字符串表示(默认是 类名@哈希码,打印对象时自动调用)、equals(obj) 判断「内容相等」(默认实现就是 ==,比地址)、hashCode() 返回用于哈希结构的整数。这三个方法的默认实现几乎都不符合业务需要,自定义类通常要重写它们——这解释了前面埋的伏笔:HashMap、HashSet 判断键是否重复靠的正是 equals 加 hashCode。
equals 与 hashCode 有一条必须遵守的约定:两个对象 equals 相等,hashCode 必须相等;反之不强制(哈希冲突允许存在)。只重写 equals 不重写 hashCode,两个「内容相等」的对象会有不同的哈希码,被 HashSet 当成两个元素、存进 HashMap 后 get 不回来——集合就「疯了」。手写这两个方法繁琐易错,工程上要么用 IDE 自动生成,要么借助 java.util.Objects 工具类:Objects.equals(a, b) 防空指针地比较字段,Objects.hash(字段...) 一行算哈希。
toString 的重写纯粹为了打印调试友好:System.out.println(obj) 时看到 "Point{x=1, y=2}" 而不是 Point@1b6d3586。建议关键领域类都重写 toString,格式简洁、包含关键字段。至于 == 与 equals 的分工再强调一次:== 比「是不是同一个对象」,equals 比「内容是否相等」;比较可能为 null 的引用时用 Objects.equals(a, b),避免空指针。
示例
重写 equals、hashCode、toString 三件套,HashSet 立刻能正确去重:
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Point other)) {
return false;
}
return x == other.x && y == other.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point{x=" + x + ", y=" + y + "}";
}
}
public class Main {
public static void main(String[] args) {
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println("== 比较:" + (p1 == p2));
System.out.println("equals 比较:" + p1.equals(p2));
System.out.println("打印对象:" + p1);
Set<Point> points = new HashSet<>();
points.add(p1);
points.add(p2);
points.add(new Point(3, 4));
System.out.println("去重后的集合:" + points);
}
}
常见坑
- 只重写 equals 不重写 hashCode:两个方法必须成对重写,否则 HashMap/HashSet 行为错乱,而且不抛异常,纯属静默故障。
- equals 参数类型写错:写成 equals(Point p) 就不是重写而是重载,Object 的版本仍在,集合照样走默认实现。参数必须是 Object,配合 @Override 让编译器把关。
- 用可变字段算 hashCode:对象进 HashSet 之后字段又被改了,哈希码跟着变,这个元素从此「找不到、删不掉」。参与 equals/hashCode 的字段最好是 final。
- 在 toString 里拼敏感信息或大对象:日志里打印出密码、身份证号是安全事故;拼接巨型列表则拖垮性能。toString 只放关键且可公开的字段。
小结
equals/hashCode 必须成对重写且基于稳定字段,Objects 工具类简化实现,toString 让调试打印可读。下一节是最后一章:把全教程的经验浓缩为编码最佳实践。