当超类没有重新声明equals()和hashCode()时会出现什么问题?

30

假设有两个类如下:

abstract class A { /* some irrelevant methods */ }

class B extends A {
    public final int x;

    public B(final int x) {
        this.x = x;
    }

    /* some more irrelevant methods */
}

我使用Eclipse的 "Source → Generate hashCode() and equals()" 在类B上生成了equals()hashCode()方法。但是Eclipse警告我:

超类'com.example.test2.A'没有重新声明equals()和hashCode() - 生成的代码可能无法正常工作。

那么,什么会导致使用生成的方法导致代码不能正常工作?


(顺便说一句,生成的方法看起来像这样:

@Override
public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + x;
    return result;
}

@Override
public boolean equals(Object obj) {
    if (this == obj)
        return true;
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    B other = (B) obj;
    if (x != other.x)
        return false;
    return true;
}

1
这可能会引起兴趣。尽管如此,我不确定它如何适用,因为A是抽象的。http://www.artima.com/lejava/articles/equality.html。大多数情况下,它试图防止`b.equals(a)`但却`!a.equals(b)`。 - SJuan76
2
我认为你的问题因为大量解释了一个非常相似但关键不同的情况而受到了影响。下面几乎每个答案都谈到了超类具有equals方法,而在你的例子中它显然没有。我甚至不是提问者,但我在这里感到沮丧! - Duncan Jones
如果你移除抽象修饰符,警告还会生成吗? - Mike
@monkybonk05 是的,没错。 - Duncan Jones
4个回答

10

当覆盖equals方法时,您必须小心遵循一组特定的规则。有关详细信息,请参见javadoc。简而言之,保留对称性和传递性是两个棘手的部分。根据Joshua Block的Effective Java

"没有办法扩展可实例化的类并添加一个值组件,同时保留等式合同"

这是什么意思?好吧,假设您在类A中有一个类型为T的属性,在子类B中有另一个类型为V的属性。如果两个类都覆盖了equals方法,则比较A和B与B和A时会得到不同的结果。

A a = new A(T obj1);
B b = new B(T obj1, V obj2);
a.equals(b) //will return true, because objects a and b have the same T reference.
b.equals(a) //will return false because a is not an instanceof B

这是对称性的违反。如果你试图通过进行混合比较来纠正此问题,你将失去传递性。
B b2 = new B(T obj1, V obj3);
b.equals(a) // will return true now, because we altered equals to do mixed comparisions
b2.equals(a) // will return true for the same reason
b.equals(b2) // will return false, because obj2 != obj3

在这种情况下,b == a,b2 == a,b != b2,这是一个问题。
编辑
为了更精确地回答问题:“什么会使生成的方法与结果代码不正确”,让我们考虑这个特定案例。父类是抽象的且没有覆盖equals。我相信我们可以得出结论,代码是安全的,并且没有违反equals合同。这是由于父类是抽象的。它无法被实例化,因此上述示例不适用于它。
现在考虑当父类是具体的且没有覆盖equals的情况。正如Duncan Jones指出的那样,警告消息仍然会生成,并且在这种情况下似乎是正确的。默认情况下,所有类都从Object继承equals,并且将基于对象标识(即内存地址)进行比较。如果与覆盖equals的子类一起使用,这可能导致不对称的比较。
A a = new A();
B b = new B(T obj1);
a.equals(b) //will return false, because the references do not point at the same object
b.equals(a) //should return false, but could return true based on implementation logic. 

如果b.equals(a)由于实现逻辑或编程错误返回true,将导致对称性的丢失。编译器无法强制执行此操作,因此会生成警告。

3
请查看此链接http://www.artima.com/lejava/articles/equality.html。基本上,子类可以通过那里提出的机制确保superInstance.equals(subInstance)为false。 - SJuan76
"无法扩展可实例化的类..." - 但是类A是抽象的。 - kennytm
1
您的回答对于这种情况不适用。超类没有equals方法。因此,您列出的示例都不相关。 - Duncan Jones
2
@monkybonk05 我还是不太信服。在 equals 方法中不比较类类型是如此基本的错误,我不确定它是否值得成为一个例子。 - Duncan Jones
2
@monkybonk05 但是,这仍然依赖于超类定义equals!抱歉,但我仍然坚定地认为警告是错误的,因为我还没有看到它如何出错的例子(除了子类中非常糟糕的equals方法)。 - Duncan Jones
显示剩余3条评论

9

我认为这个警告是错误的。我曾经看到过关于equals构造的所有文献 (包括 Bloch 的第8项) ,都警告了父类确实 实现了 equals 的情况。

鉴于你的A类仅使用引用相等性,我无法想象在任何需要遵守的原则(对称性、传递性和自反性)中,你的B equals方法会违反任何一个原则。

请记住,任何半明智的equals方法都将进行类型检查。这也适用于原始问题中的自动生成代码:

if (getClass() != obj.getClass())
    return false;

我们不仅仅应该引用Bloch的书和其他网站的内容,而是应该认真思考:如果父类没有实现equals方法,是否可能出现任何“标准”等号问题。我认为不可能,并欢迎反例。


超类 A 没有实现 equals 方法,而子类 B 实现了它(通过检查 id),因此可能存在这样的情况,即 b.equals(a) 因为 id 匹配而成立,但 !a.equals(b) 因为它们是不同的对象。 - SJuan76
@SJuan76 只有一个糟糕编码的 equals 方法,它不会比较类类型。 - Duncan Jones
请查看我提供的链接,以更好地解释在一个编码不那么糟糕的实现中如何发生。 - SJuan76
@SJuan76 是的,但是那个链接只有在超类定义了一个equals方法时才有效。而在这个问题中并非如此。 - Duncan Jones
@monkybonk05 我的想法没有改变,恐怕是这样的 :-) - Duncan Jones

1
如果父类和子类都以这样一种方式实现equals,即除非两个引用都指向完全相同的类的实例,否则不可能将两个引用视为相等,那么就不会有关于继承的equals问题。
当存在可能具有不同类但仍应该比较相等的实例时,出现了问题。 处理这种情况的唯一正确方法是指定任何可以相互比较相等的实例必须派生自一个公共类,并且该类的契约必须指定相等的含义。 公共类equals(可以是抽象的或非抽象的)通常将定义虚成员,派生类可以重写以测试不同方面的相等性。 例如,如果公共类equals方法类似于if (other==null) return false; else return this.equals2(other) && other.equals2(this);,那么这基本上保证了对于不会改变正在比较的对象的任何equals2实现而言,都具有对称行为。

-3

等待了一段时间,直到有一个令人信服的理由,我的看法。

我链接的文章(再次提醒,它是http://www.artima.com/lejava/articles/equality.html)解释了为什么在子类中实现equals存在问题(主要是因为instanceof检查会导致违反equals契约的不对称性)。

如果你的超类没有实现equals,你可能会遇到这种情况。

  • B继承A

  • B实现equals,就像这样

    public boolean equals(Object obj) {
      A a = (A) obj;  <-- 这个强制类型转换是这个例子中最棘手的问题
      return this.id.equals(a.getId());
    }
    

所以你最终得到

A a = new A("Hello");
B b = new B("Hello");
a.equals(b) != b.equals(a);

一个反对这个例子的观点是,你的超类是抽象的,但可能是Eclipse发出警告,以防止你实例化它(或者只是警告检查不够精细)。

由于A是抽象的,Eclipse不会提供警告。有关警告的实际原因,请参见我的答案。 - rahul maindargi
1
我认为你的equals示例不必要地差劲。为什么B类中的equals方法会将对象强制转换为A?任何明智的equals方法的核心都是一个类检查。我觉得你已经调整了你的示例以适应你认为的答案。 - Duncan Jones
1
@Duncan,我非常确定你是对的(尽管没有人同意你的观点)。超类不实现equals肯定不是问题(Object也没有实现它)。重写一个非平凡的equals可能会成为一个问题,正如链接文章中所解释的那样,但他们提供了一个通用的解决方案(canEqual)。不幸的是,这篇文章写得相当混乱。 - maaartinus

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