在继承层次结构中实现健壮的 equals() 和 hashCode() 方法的正确方法是什么?

What is the proper way to implement a robust equals() and hashCode() method in an inheritance hierarchy?

我有以下摘要Person class:

import java.util.Objects;

public abstract class Person {

    protected String name;
    protected int id;

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

    public abstract String description();

    @Override
    public boolean equals(Object obj) {
        if(this == obj) return true;
        if(!(obj instanceof Person)) return false;

        return Objects.equals(this.name, ((Person) obj).name) &&
                this.id == ((Person) obj).id;
    }

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

现在我有一个 Person 的子 class 叫做 Employee:

import java.time.LocalDate;
import java.util.Objects;

public class Employee extends Person {

    private double salary;
    private LocalDate hireDay;

    public Employee(String name, int id, double salary, int year, int month, int day) {
        super(name, id);
        this.salary = salary;
        this.hireDay = LocalDate.of(year, month, day);
    }

    @Override
    public String description() {
        return "Employee with a salary of " + this.salary;
    }

    @Override
    public int hashCode() {
        return super.hashCode() + Objects.hash(this.salary,this.hireDay);
    }

    @Override
    public boolean equals(Object obj) {

        return super.equals(obj) && 
        Double.compare(this.salary, ((Employee) obj).salary) == 0
              && Objects.equals(this.hireDay,((Employee)obj).hireDay);

}

要正确实现 equals 方法,它必须符合以下约定。

Reflextive: x.equals(x) is always True
Symmetric: x.equals(y) is equivalent to y.equals(x)
Transitive: x.equals(y) and y.equals(z) implies x.equals(z) is true

当我在 subclass 中调用 superclass 的 equals() 方法时,我首先确保所有被比较的对象都是 superclass 的子classes =45=]。这个问题解决了比较混合类型的问题,并处理了上面提到的契约。我不再需要使用 equals 的以下实现:

    @Override
    public boolean equals(Object obj) {

        if(this == obj) return true;
        else if(obj == null || this.getClass() != obj.getClass()) return false;

        Employee other = (Employee) obj;

        return Objects.equals(this.name, other.name) &&
               Double.compare(this.salary, other.salary) == 0 &&
               Objects.equals(this.hireDay, other.hireDay);

    }

即,由于 super[=45 中的方法,我不再需要显式检查当前对象 (this) 是否与 obj 相同 class =] 使用 instance of 运算符。

将该实现放在 superclass 的等号运算符中是否更稳健,还是使用 getClass() 在 subclass 中使用更明确的测试更好方法才能符合约定?

就 hashCode() 方法而言,我对特定于 subclass 的私有实例字段进行哈希处理,并将其简单地添加到 superclass 中的哈希方法的结果中。我找不到任何文档来说明这是否是在一般情况下或在继承层次结构中实现 hashCode() 函数的正确方法。我见过人们明确指定自己的哈希函数的代码。

如果我的问题过于笼统,我深表歉意,但我已尽我最大的努力去问,不要太模棱两可。

编辑:

我要求 Intellij 实现一个 equals 和 hashcode 方法,它决定采用我在上面发布的最后一个实现。那么什么情况下我会在superclass中使用instance of呢?会不会是当我在 superclass 中实现最终的 equals 方法时,例如只比较基于用户 ID 的 Person 对象?

两个人有可能永远拥有相同的id吗?它不应该。因此,该逻辑扩展到 Employee class,这意味着在 Person class 中实现 equalshashCode 就足够了。

此时,由于您只处理 int,您可以使用 Integer.hashCode(id) 作为 hashCode 并比较 equals 的值.

如果你想使用 eclipse 实现 equals 和 hashcode 方法,只需右键单击文件转到源代码,然后 select 使用你需要的字段生成 equals() 和 hashcode(),如下所示:

这是我阅读 Effective Java 第二版时的笔记:

等于 必须遵守总合同:

  • 自反:对于 non-null xx.equals(x) == true
  • 对称:non-null x,yx.equals(y) <==> y.equals(x)
  • 传递:对于non-null x,y,zx.equals(y) and y.equals(z) ==> x.equals(z) == true
  • 一致:对于任何非空 x,y:如果 x.equals(y) == true,则如果 xy[] 没有变化,则所有调用必须 return 为真=90=]
  • null: 对于非null x: x.equals(null) == false

高质量等于方法:

  1. 使用 == 检查参数是否是对该对象的引用 (x == x)
  2. 使用 instanceof 检查参数类型是否正确(同时检查 null
  3. 将参数转换为正确的类型
  4. 对于 class 中的每个 "significant" 字段,检查参数的该字段是否与该对象的相应字段匹配
  5. 完成后,检查是否对称、传递和一致

最后的警告:

  • 在覆盖 equals 时始终覆盖 hashCode
  • 不要自作聪明
  • 不要在 equals 声明中用其他类型替换 Object -> 不值得为了增加复杂性而获得较小的性能提升

Hashcode 直接引自 Effective Java 第二版

  1. 在名为 result 的 int 变量中存储一些常量非零值,例如 17。
  2. 对于对象中的每个重要字段 f( equals 方法,即),执行以下操作:

    • 计算字段的 int 哈希码 c:
      1. 如果字段是布尔值,计算 (f ? 1 : 0)
      2. 如果字段是 byte, char, short, or int, compute (int) f.
      3. 如果字段是 long, compute (int) (f ^ (f >>> 32)).
      4. 如果字段是 float, compute Float.floatToIntBits(f).
      5. 如果字段是 double, compute Double.doubleToLongBits(f),并且 然后散列结果 long.
      6. 如果字段是一个对象引用并且这个class的equals方法 通过递归调用 equals 来比较字段,递归地 在字段上调用 ​​hashCode。如果更复杂的比较是 需要,计算该字段的“规范表示”和 在规范表示上调用 hashCode。如果该值 字段是 nullreturn 0(或其他一些常量,但 0 是传统的)。
      7. 如果该字段是一个数组,则将其视为每个元素都是一个单独的字段。 也就是说,通过应用计算每个重要元素的哈希码 这些规则递归,并在每一步 2.b 中组合这些值。如果每 数组字段中的元素很重要,您可以使用其中一个 Arrays.hashCode 方法在版本 1.5 中添加。
    • 将步骤2.a计算出的哈希码c合并为结果如下: result = 31 * result + c;
  3. Return结果。

  4. 当你写完 hashCode 方法后,问问自己是否 相等的实例具有相等的哈希码。编写单元测试来验证您的直觉!

所以遵循这些规则:

@Override
public boolean equals(Object obj) {

    if (this == obj) {
        return true;
    }

    if (!(obj instanceof Employee)) {
        return false;
    }
    Employee other = (Employee) obj;

    return super.equals(other) &&
           Double.compare(this.salary, other.salary) == 0 &&
           this.hireDay.equals(other.hireDay);

}

在你的情况下,虽然看起来 id 应该已经唯一地标识了任何人,所以你应该只使用它来比较 equals 而不是在任何 subclass.