讲解

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 让调试打印可读。下一节是最后一章:把全教程的经验浓缩为编码最佳实践。