CodeGym /课程 /JAVA 25 SELF /equals, hashCode, toString:自动生成

equals, hashCode, toString:自动生成

JAVA 25 SELF
第 22 级 , 课程 2
可用

1. equals、hashCode、toString 的自动生成

为什么需要这些方法?

在 Java 中处理对象时,你很快就会遇到一些重复性的任务。有时需要检查两个对象是否相等,例如判断它是否已经存在于类似于 SetMap 的集合中。还有些场景对象会被用作 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()
  • equalshashCodetoString

自动生成的方法

  • 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 的元素。如果 equalshashCode 实现不正确,集合会表现得很奇怪:要么找不到你刚放进去的元素,要么把两个不同的对象当成相同的。

使用 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!" — 正常工作!
    }
}

请注意:p1p2 是不同的对象(不同引用),但字段值相同,因此被视为相等。关于 MapHashMap 的更多内容,你将在第 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
    }
}

如果 equalshashCode 没有正确实现,查找就不会成功。关于 MapHashMap 的更多内容,请参见第 26 级的课程 :P

6. 使用 record 类中的 equals、hashCode、toString 时的常见错误

错误 1:以为字段在创建后还能修改。
record 的字段始终是 final,并按构造器设置的值进行比较。如果你通过某些“技巧”去“改变”内部状态(例如字段里持有可变对象且其内容被修改),比较与哈希可能会变得不正确。

错误 2:重写了 equals,却忘了 hashCode。
如果你重写了这两个方法之一——一定要把另一个也一起重写!否则集合(HashSetHashMap)会表现得不可预测。

错误 3:期待 toString 采用其他格式。
如果你需要特殊的字符串格式——直接重写 toString 即可。默认格式始终是 ClassName[field1=value1, field2=value2]

错误 4:把 record 用于包含可变字段的复杂类。
record 的字段应该是不可变的。如果字段是例如 ArrayList 之类的可变类型,且有人修改了其内容——比较与哈希码可能会“失效”。对 record 而言,最好只使用不可变类型。

错误 5:将 record 用于并非值对象(value object)的类。
record 不是“语法更短的小类”。它就是值对象(value object),用于保存一组值。如果你的类包含复杂逻辑、可变状态或需要继承——请使用普通类。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION