在子类中覆盖equals()和hashCode()方法…考虑超类字段

75

在子类中覆盖 equals()hashCode() 方法时,是否有特定的规则考虑继承字段?知道有很多参数:继承字段是私有的/公共的,有/没有getter等。

例如,Netbeans 生成的 equals() 和 hashCode() 不会考虑父类的字段...

    new HomoSapiens("M", "80", "1.80", "Cammeron", "VeryHot").equals(
    new HomoSapiens("F", "50", "1.50", "Cammeron", "VeryHot"))

会返回 true :(

public class Hominidae {

    public String  gender;
    public String  weight;
    public String  height;

    public Hominidae(String gender, String weight, String height) {
        this.gender = gender;
        this.weight = weight;
        this.height = height;
    }
    ... 
}

public class HomoSapiens extends Hominidae {
    public String name;
    public String faceBookNickname;

    public HomoSapiens(String gender, String weight, String height, 
                       String name, String facebookId) {
        super(gender, weight, height);
        this.name = name;
        this.faceBookNickname = facebookId;
    }
    ...  
}

如果你想查看Netbeans生成的equals()和hashCode()方法:

public class Hominidae {

    ...

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Hominidae other = (Hominidae) obj;
        if ((this.gender == null) ? (other.gender != null) : !this.gender.equals(other.gender)) {
            return false;
        }
        if ((this.weight == null) ? (other.weight != null) : !this.weight.equals(other.weight)) {
            return false;
        }
        if ((this.height == null) ? (other.height != null) : !this.height.equals(other.height)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 5;
        hash = 37 * hash + (this.gender != null ? this.gender.hashCode() : 0);
        hash = 37 * hash + (this.weight != null ? this.weight.hashCode() : 0);
        hash = 37 * hash + (this.height != null ? this.height.hashCode() : 0);
        return hash;
    }

}


public class HomoSapiens extends Hominidae {

    ...

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final HomoSapiens other = (HomoSapiens) obj;
        if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) {
            return false;
        }
        if ((this.faceBookNickname == null) ? (other.faceBookNickname != null) : !this.faceBookNickname.equals(other.faceBookNickname)) {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 89 * hash + (this.name != null ? this.name.hashCode() : 0);
        hash = 89 * hash + (this.faceBookNickname != null ? this.faceBookNickname.hashCode() : 0);
        return hash;
    }
}

我认为智人并不是人科的延伸。至少还应该有直立人。 - Pavel_K
10个回答

73

子类不应该检查其父类的私有成员变量

但是显然,所有重要字段都应考虑在内以实现相等和哈希。

幸运的是,你可以很容易地满足这两个规则。

假设你没有被限制使用NetBeans生成的equals和hashcode方法,那么你可以修改Hominidae的equals方法,使用instanceof比较而不是类相等性,然后直接使用它。例如:


    @Override  
    public boolean equals(Object obj) {  
        if (obj == null) { return false; }  
        if (getClass() != obj.getClass()) { return false; }  
        if (! super.equals(obj)) return false;
        else {
           // compare subclass fields
        }

当然,哈希码很容易:


    @Override     
    public int hashCode() {     
        int hash = super.hashCode();
        hash = 89 * hash + (this.name != null ? this.name.hashCode() : 0);     
        hash = 89 * hash + (this.faceBookNickname != null ? this.faceBookNickname.hashCode() : 0);     
        return hash;     
    }     

说真的,NetBeans为什么不通过调用超类方法来考虑超类字段?


71
我认为“孩子不应该检查父母的私处”是非常明智的家长建议。 - OliCoder
21
这个 equals 方法不符合等式契约。它不是对称的,因为由于 getClass() 检查,subclassInstance.equals(parentInstance) 永远不会返回 true,但在你使用 super.equals 的时候隐含了这样一个假设:parentInstance.equals(subclassInstance) 可以返回 true。 - MikeFHay
2
如果 super#equals 的工作方式相同,那么 super.equals(obj) 总是返回 false - Jin Kwon
1
我为NetBeans打开了一个问题 https://issues.apache.org/jira/browse/NETBEANS-4512 - Pavel_K
1
@MikeFHay super.equals 只会针对同一类的实例进行调用,即使它在父类的 equals 方法中,this.getClass() 仍然会返回子类。这里并没有破坏对称性。 - algrid
显示剩余7条评论

23

我更喜欢使用EqualsBuilder(和HashcodeBuilder)来自commons-lang包,使我的equals()和hashcode()方法更易读。

例子:

public boolean equals(Object obj) {
 if (obj == null) { return false; }
 if (obj == this) { return true; }
 if (obj.getClass() != getClass()) {
   return false;
 }
 MyClass rhs = (MyClass) obj;
 return new EqualsBuilder()
             .appendSuper(super.equals(obj))
             .append(field1, rhs.field1)
             .append(field2, rhs.field2)
             .append(field3, rhs.field3)
             .isEquals();
}

2
赞赏你提到了.appendSuper() - Felipe Leão

11

通常来说,在子类中实现equals方法很难保持对称和传递性。

考虑一个超类,它检查字段x和y,而子类检查x、y和z。

因此,如果在子类的第一个实例和第二个实例之间的z不同,就会违反传递性,导致Subclass == Superclass == Subclass 但结果为false。

这就是为什么典型的equals实现会检查getClass() != obj.getClass()而不是使用instanceof。在上面的例子中,如果SubClass或Superclass执行instanceof检查,它将破坏对称性。

因此,子类确实可以考虑使用super.equals(),但也应该进行自己的getClass()检查以避免以上问题,并额外检查其自己的字段是否相等。如果一个类基于超类的特定字段而更改其自身的equals行为,而不只是像超类一样返回equals,那将是一个奇怪的类。


谢谢Yishai,但这里的问题是比较两个子类的实例,问题在于我们不能像"super.equals(obj.super)"这样做,其中obj是被比较的对象。 - wj.
@wj,只要你的类和obj类相同,我不明白为什么你不能调用`if (!super.equals(obj)) return false'。 - Yishai
@Yishai,这里的重点是找到一种比较"this.super"和"obj.super"而不是"this.super"和"obj"的方法,因为它们不是同一个类的直接实例,这意味着"super.equals(obj)"总是false...在我的例子中,"this.super"是"Hominidae",而"obj"是"HomoSapiens"。 - wj.
@wj,我觉得你没有理解我的意思。我建议的是与@matt b答案中Equals构建器中的appendSuper方法相同的事情。 - Yishai
@Yishai,你有什么建议??? - Jack

7
规则如下:
  • 它是反身性的:对于任何非空引用值x,x.equals(x)应返回true。
  • 它是对称的:对于任何非空引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)应返回true。
  • 它是传递性的:对于任何非空引用值x、y和z,如果x.equals(y)返回true并且y.equals(z)返回true,则x.equals(z)应返回true。
  • 它是一致的:对于任何非空引用值x和y,只要在对象上使用equals比较的信息未被修改,x.equals(y)的多次调用始终返回true或始终返回false。
  • 对于任何非空引用值x,x.equals(null)应返回false。
  • 通常需要重写hashCode方法,以便在重写此方法时维护hashCode方法的一般契约,该契约指出相等的对象必须具有相等的散列码
来自Object.equals()
因此,请使用需要的字段来满足这些规则。

查看规范而不是(或者除了)实现和经验总是一个好习惯。 - Jonathan Rosenne
请注意,产生相同 hashCode 的两个对象不一定相等。 - Jin Kwon

4
好的,根据CPerkins的回答,“HomoSapiens#hashcode”就足够了。
@Override     
public int hashCode() {     
    int hash = super.hashCode();
    hash = 89 * hash + Objects.hash(name);     
    hash = 89 * hash + Objects.hash(faceBookNickname);     
    return hash;     
}

如果您想要使用这些父级字段(genderweightheight),一种方法是创建父类型的实际实例并使用它。感谢上帝,它不是抽象类。

@Override
public boolean equals(Object obj) {
    if (obj == null) {
        return false;
    }
    if (getClass() != obj.getClass()) {
        return false;
    }
    final HomoSapiens other = (HomoSapiens) obj;
    if (!super.equals(new Hominidae(
        other.gender, other.weight, other.height))) {
         return false;
    }
    if (!Objects.equals(name, other.name)) return false;
    if (!Objects.equals(faceBookNickname, other.faceBookNickname))
        return false;
    return true;
}

我正在添加一种(我认为)可以解决这个问题的方法。关键在于添加一个方法,它会松散地检查等式。

public class Parent {

    public Parent(final String name) {
        super(); this.name = name;
    }

    @Override
    public int hashCode() {
        return hash = 53 * 7 + Objects.hashCode(name);
    }

    @Override
    public boolean equals(final Object obj) {
        return equalsAs(obj) && getClass() == obj.getClass();
    }

    protected boolean equalsAs(final Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (!getClass().isAssignableFrom(obj.getClass())) return false;
        final Parent other = (Parent) obj;
        if (!Objects.equals(name, other.name)) return false;
        return true;
    }

    private final String name;
}

现在出现了一个Child

public class Child extends Parent {

    public Child(final String name, final int age) {
        super(name); this.age = age;
    }

    @Override
    public int hashCode() {
        return hash = 31 * super.hashCode() + age;
    }

    @Override
    public boolean equals(final Object obj) {
        return super.equals(obj);
    }

    @Override
    protected boolean equalsAs(final Object obj) {
        if (!super.equalsAs(obj)) return false;
        if (!getClass().isAssignableFrom(obj.getClass())) return false;
        final Child other = (Child) obj;
        if (age != other.age) return false;
        return true;
    }

    private final int age;
}

测试中...

@Test(invocationCount = 128)
public void assertReflective() {
    final String name = current().nextBoolean() ? "null" : null;
    final int age = current().nextInt();
    final Child x = new Child(name, age);
    assertTrue(x.equals(x));
    assertEquals(x.hashCode(), x.hashCode());
}

@Test(invocationCount = 128)
public void assertSymmetric() {
    final String name = current().nextBoolean() ? "null" : null;
    final int age = current().nextInt();
    final Child x = new Child(name, age);
    final Child y = new Child(name, age);
    assertTrue(x.equals(y));
    assertEquals(x.hashCode(), y.hashCode());
    assertTrue(y.equals(x));
    assertEquals(y.hashCode(), x.hashCode());
}

@Test(invocationCount = 128)
public void assertTransitive() {
    final String name = current().nextBoolean() ? "null" : null;
    final int age = current().nextInt();
    final Child x = new Child(name, age);
    final Child y = new Child(name, age);
    final Child z = new Child(name, age);
    assertTrue(x.equals(y));
    assertEquals(x.hashCode(), y.hashCode());
    assertTrue(y.equals(z));
    assertEquals(y.hashCode(), z.hashCode());
    assertTrue(x.equals(z));
    assertEquals(x.hashCode(), z.hashCode());
}

2

就 @CPerkins 的回答而言,我认为给出的 equals() 代码不可靠,因为超类的 equals() 方法可能也会检查类的相等性。子类和超类将不具有相等的类。


1
这不是一个答案,而是一条评论。尽管如此,它是非常有效的评论。 - Adriaan Koster
超类的super.equals()和super.hashcode()应该被编写成这样的方式,使它们适用于子类,如果不适用,则应该是final。 - Jonathan Rosenne
2
有点晚了,但仍然可以解决。毕竟:当您调用super.equals时,仍然是从子类的实例调用它。包含(this.)getClass()的超类并不改变方法在实例上被调用的事实,以及该实例的类不会改变。 - Stultuske
是的,被投票否决了 :) - Jack

2
值得注意的是,IDE自动生成可能已经考虑了父类,只要父类的equals()和hashCode()方法已经存在。也就是说,应该先自动生成这两个函数的父类,然后再自动生成子类。在Intellj Idea下,我得到了以下正确的示例:
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    if (!super.equals(o)) return false;

    TActivityWrapper that = (TActivityWrapper) o;

    return data != null ? data.equals(that.data) : that.data == null;
}

@Override
public int hashCode() {
    int result = super.hashCode();
    result = 31 * result + (data != null ? data.hashCode() : 0);
    return result;
}

问题发生在您没有首先自动生成super的情况下。请在Netbeans上方检查上述内容。

1
不行。看看你的equals()实现的第二行。如果超类有相同的行(应该有),那么对super.equals(o)的调用将会失败,因为它们的类型不同。子类真正需要比较定义相等性的所有属性,而不仅仅是本地声明的属性。甚至通过getter也无法访问的私有字段在子类中也不应该成为相等检查的一部分,从语义上讲,为什么要关心看不到的差异呢? - ideasculptor
super.equals可能会导致Object#equals的身份验证...请小心。 - Max

1

由于继承会破坏封装性,实现equals()和hashCode()方法的子类必须考虑到其父类的特殊性。我已经成功地在子类的方法中编码调用父类的equals()和hashCode()方法。


同样地,在Hominidae中重写equals()hashCode()方法将使得在HomoSapiens中对应的方法相对容易实现。 - trashgod
@trashgod:在Hominidae中覆盖equals()和hashCode()方法并不能使HomoSapiens中对应的方法更容易实现,因为我们无法像“super.equals(obj.super)”这样做,其中obj是被比较的对象... - wj.
@wj:你说得对。我在想这个例子,它调用了String相应的方法:http://stackoverflow.com/questions/1924728/why-isnt-collections-binarysearch-working-with-this-comparable/1926111#1926111 - trashgod
@trashgod:事实上,我完全错了...而你绝对是正确的 :) - wj.
有什么建议吗,朋友? - Jack

1

看起来你的父类没有覆盖equals方法。如果是这种情况,那么在子类中重写此方法时,需要比较父类的字段。我同意使用commons EqualsBuiler是正确的方法,但您需要小心,不要破坏equals合同的对称性/传递性部分。

如果您的子类向父类添加属性,并且父类不是抽象类并覆盖了equals方法,则会遇到麻烦。在这种情况下,您应该真正考虑对象组合而不是继承。

我强烈推荐Joshua Block在Effective Java中的这一部分。它非常全面,解释得非常好。


0
我相信现在有一种方法可以为您完成这个操作:
EqualsBuilder.reflectionEquals(this, o);

HashCodeBuilder.reflectionHashCode(this);


1
反射是非常昂贵的,这2个方法必须以优化性能的方式实现。 - Max
显然,显而易见的是显而易见的。但说真的,我不认为这些方法所做的反射在性能上会比编写代码来做更加昂贵。 - Terry H
1
  1. 如果你有一个 HashSet,想要添加一个元素,那么 hashCodeequals 可能会被频繁使用。
  2. 如果你使用反射实现了你的 hashCode 和/或 equals 方法,那么你可以考虑所有对象的实例变量。这样你就可以避免一些问题(我有一个可怕的例子:一个包含元数据和文件/流的对象)。
- Max
然而,这些似乎都不重要。你可以想出所有可怕的情况,但如果你需要让你的等式或哈希函数考虑到所有这些事情,那么你必须考虑它们。你可以使用内置的反射或编写代码来实现,但必须完成它。我发现很少有人能够编写比这些函数内置更高效的代码。当然,这是可能的,但在实践中很少发生。 - Terry H

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接