CodeGym /课程 /JAVA 25 SELF /equals 与 hashCode 的契约

equals 与 hashCode 的契约

JAVA 25 SELF
第 29 级, 课程 0
可用

1. 引言

我们如何比较对象

在 Java 中,对象不仅仅是数据,每个对象在内存中都有自己的地址。运算符 == 回答的是“这是同一个盒子吗?”这个问题,也就是说比较的是引用(地址),而不是内容。方法 equals 则用于比较内容。本质上,如果不重写,equals 的行为与 == 相同。

Person p1 = new Person("Ivan", 20);
Person p2 = new Person("Ivan", 20);

System.out.println(p1 == p2); // false — 这是内存中的不同对象!

要让两个不同的对象按数据被视为相等(例如所有重要字段都相同),需要重写 equals。并且如果你计划在哈希集合中使用这些对象,务必要与之配套正确地重写 hashCode

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 这是同一个对象
        if (o == null || getClass() != o.getClass()) return false; // 检查类
        Person person = (Person) o; // 转为所需类型
        return age == person.age && name.equals(person.name); // 比较字段
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); // 使其可用于 HashSet/HashMap
    }
}

这种重写对于在 HashSet/HashMap 中的正确行为至关重要:没有正确的 equalshashCode,集合会把数据相同的对象也当作不同的对象。

equals 中的等价关系取决于你的需求:可以按所有字段比较,也可以按部分字段(例如对 User 仅比较 email)——关键是保持一致并遵守契约。

在哪些场景尤其重要?

  • 基于哈希表的集合:HashSetHashMapLinkedHashSet 等。
  • 在集合中查找和删除元素:如果没有正确的 equals,目标对象可能“找不到”。
  • 在业务逻辑中:例如两个拥有相同 email 的 User 应视为同一用户。

hashCode——它有什么用?

哈希集合(例如 HashSetHashMap)使用哈希表。方法 hashCode 计算一个整数——“桶的地址”,对象将被放入该桶。如果两个对象按 equals 相等,那么它们的 hashCode 必须相同。违反这个规则——集合就会变得不可预测。

2. equals 和 hashCode 的契约

equals 的契约

equals 的基本要求:

  • 自反性:a.equals(a) 始终为 true。
  • 对称性:如果 a.equals(b) 为 true,那么 b.equals(a) 也为 true。
  • 传递性:如果 a.equals(b)b.equals(c),则 a.equals(c) 也为 true。
  • 一致性:在对象未发生变化时,多次调用结果应稳定。
  • 与 null 的比较:任何对象都不等于 null

hashCode 的契约

  • 如果两个对象按 equals 相等,它们的 hashCode 必须相等。
  • 若对象不相等,它们的哈希码可能碰撞(允许但不理想)。
  • 在对象逻辑未改变的情况下,其 hashCode 应保持不变。

换言之,相同的 hashCode 是相等的必要但非充分条件:相同哈希并不保证按 equals 相等。

3. 实现 equals 与 hashCode:示例

Person 类为例,其相等性由 nameage 字段决定。

public class Person {
    private String name;
    private int age;

    // 构造器、getter、setter...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 比较引用
        if (o == null || getClass() != o.getClass()) return false; // 检查类

        Person person = (Person) o; // 类型转换

        // 比较字段
        return age == person.age &&
               (name != null ? name.equals(person.name) : person.name == null);
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age; // 31 是常用的素数选择
        return result;
    }
}
  • 先进行快速检查:引用与类。
  • 然后比较重要字段。
  • hashCode 中使用素数 31 以减少碰撞。

使用 Objects.equals 和 Objects.hash

自 Java 7 起,Objects 类可简化代码,并使其对 null 更加安全:

import java.util.Objects;

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return age == person.age &&
           Objects.equals(name, person.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

4. equals、hashCode 与 compareTo:它们如何关联

equals 与 compareTo 如何关联?

接口 Comparable 定义了方法 compareTo,其返回负数/零/正数分别表示“小于/等于/大于”。通常希望由 a.compareTo(b) == 0 能推出 a.equals(b)。反之不必然成立。

如果破坏了一致性(例如 compareTo 只按年龄比较,而 equals 按姓名与年龄比较),那么诸如 TreeSet/TreeMap 的可排序集合可能会出现意外行为:从顺序的角度看对象“相等”,但从内容上并不相等。

集合中的 equals 与 hashCode

  • HashSetHashMap 中,添加/查找/删除操作依赖于正确实现的 equalshashCode
  • 如果不重写,这些集合会把对象视为“不同”,即便它们的数据相同。

5. 示例:在集合中如何工作

HashSet:存储唯一对象

Set<Person> people = new HashSet<>();
people.add(new Person("Ivan", 20));
people.add(new Person("Ivan", 20)); // 重复项

System.out.println(people.size()); // 如果 equals/hashCode 实现正确则为 1

如果没有正确的 equals/hashCode,集合会接收两个对象。

HashMap:按键查找

Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Anna", 25);
Person p2 = new Person("Anna", 25);

map.put(p1, "用户 1");
System.out.println(map.get(p2)); // "用户 1",如果 equals/hashCode 实现正确

没有契约,集合会返回 null——对它来说这是“不同”的键。

6. 最佳实践:实现建议

  • equals/hashCode 中纳入所有决定对象“标识”的字段。
  • 不要在计算 hashCode 时使用会在放入集合后改变的可变字段。
  • 让 IDE 生成这些方法——能降低手误风险。
  • equals 中先检查 this == o,再检查类,最后比较字段。
  • 比较对象字段时使用 Objects.equals
  • 计算哈希码时使用 Objects.hash 或采用乘数 31 的成熟模板。

7. 有用的细节

为什么不能只用 hashCode?

碰撞不可避免:不同对象可能拥有相同的 hashCode。哈希只是用于定位桶的快捷线索;最终的相等判定由 equals 做出。

是否可以不重写 equals 和 hashCode?

只有当你确信对象永远不会按内容被比较,且不会成为集合中的键或唯一元素时才可以。实际中这很少见。

==、equals 与 compareTo 的区别

运算符/方法 比较什么? 用途
==
引用(内存地址) 检查“是不是同一个对象?”
equals
对象内容 基于业务逻辑的相等性
compareTo
顺序(小于/等于/大于) 排序、建立顺序

8. 实现 equals 和 hashCode 的常见错误

错误 #1:重写了 equals,却忘了 hashCode。 对象被视为相等,但会落入哈希表的不同桶——查找与删除将会出问题。

错误 #2:在 hashCode 中使用了可变字段。 如果字段在放入集合后发生变化,对象会“丢失”:哈希变了,而桶没有变。

错误 #3:破坏了 equals 的对称性/传递性。 a.equals(b) 为 true,而 b.equals(a) 为 false,或传递性被破坏——集合将表现得不可预测。

错误 #4:在 equals 中未检查类。 比较不同类的对象会导致错误结果或异常。

错误 №5:用 == 比较字符串和对象。 运算符 == 比较的是引用;请使用 equals 来比较内容。

错误 №6:compareTo 与 equals 不一致。 如果 a.compareTo(b) == 0,但 !a.equals(b),那么 TreeSet/TreeMap 可能会把元素视为在顺序上“相等”但在相等性上不同——这会引发“幽灵元素”和重复项。

1
任务
JAVA 25 SELF, 第 29 级, 课程 0
已锁定
虚拟世界中的唯一城市统计 🗺️
虚拟世界中的唯一城市统计 🗺️
1
任务
JAVA 25 SELF, 第 29 级, 课程 0
已锁定
在新的高效 HR 系统中管理员工 👥
在新的高效 HR 系统中管理员工 👥
评论 (1)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
ncksllpo 级别 44,Cherkasy,Ukraine
22 二月 2026
为啥不让用record