23

我正在尝试编写一些通用代码来定义基于字段列表的类相等和哈希码。在编写我的 equals 方法时,我想知道根据 Java 约定,两个不同的对象是否应该相等。让我举几个例子;

class A {
  int foo;
}
class B {
  int foo;
}
class C extends A {
  int bar;
}
class D extends A {
  void doStuff() { }
}
...
A a = new A(); a.foo = 1;
B b = new B(); b.foo = 1;
C c = new C(); c.foo = 1; c.bar = 2;
D d = new D(); d.foo = 1;

a.equals(b); //Should return false, obviously
a.equals(c);
c.equals(a); //These two must be the same result, so I'd assume it must be false, since c cant possible equal a
a.equals(d); //Now this one is where I'm stuck. 

我认为没有理由在最后一个示例中两者不应该相等,但它们确实有不同的类。任何人都知道什么约定?如果它们相等,equals 方法应该如何处理呢?

编辑:如果有人对此问题背后的代码感兴趣,请参阅:https ://gist.github.com/thomaswp/5816085这有点脏,但我欢迎对要点发表评论。

4

11 回答 11

23

它们可能是,但在这种情况下通常很难保持相等的对称和传递属性。至少在对平等有一个有用/直观的定义时。

如果您允许子类认为自己等于超类的一个实例,那么超类需要认为自己等于子类的一个实例。这意味着您将在超类中编码有关子类(所有可能的子类?)的特定知识,并根据需要进行向下转换,这不是很干净。

或者,您只使用 中包含的字段进行比较A,并且根本不覆盖equals()。这解决了上述问题,但问题是C具有不同值的两个实例bar将被视为相等,这可能不是您想要的。

或者,您覆盖 in C,并比较bar另一个对象是否是 的实例C,否则不要为 的实例A,您还有另一个问题。 c1.equals(c2)会是假的,但c1.equals(a)会是真的,就像c2.equals(a)and so一样a.equals(c2)。这打破了传递性(因为c1 == aa == c2暗示c1 == c2)。


总之,这在理论上是可能的,但你必须削弱你的 equals 实现才能这样做。此外,运行时类与对象的属性一样多bar,所以我希望具有不同具体类的对象无论如何都不相等。

于 2013-06-19T17:08:04.840 回答
4

首先注意:当你覆盖时.equals(),你绝对必须覆盖.hashCode(),并遵守定义的合同。这不是开玩笑。如果你不遵守那,你注定会遇到问题。

至于处理不同继承类之间的相等性,如果所有这些类都有一个共同的成员,则可以这样做:

@Override
public int hashCode()
{
    return commonMember.hashCode();
}

@Override
public boolean equals(final Object o)
{
    if (o == null)
        return false;
    if (this == o)
        return true;
    if (!(o instanceof BaseClass))
        return false;
    final BaseClass other = (BaseClass) o;
    return commonMember.equals(other.commonMember); // etc -- to be completed
}
于 2013-06-19T17:11:45.013 回答
1

还要记住,您需要遵循这些规则才能正确实现 equals 方法。

  1. 自反:对象必须等于自身。
  2. 对称:如果 a.equals(b) 为真,则 b.equals(a) 必须为真。
  3. 传递性:如果 a.equals(b) 为真且 b.equals(c) 为真,则 c.equals(a) 必须为真。
  4. 一致:多次调用 equals() 方法必须产生相同的值,直到任何属性被修改。因此,如果两个对象在 Java 中是相等的,它们将保持相等,直到其中任何一个属性被修改。
  5. Null 比较:将任何对象与 null 进行比较必须为 false,并且不应导致 NullPointerException。例如 a.equals(null) 必须为 false,将可能为 null 的未知对象传递给 Java 中的 equals 实际上是避免 Java 中出现 NullPointerException 的 Java 编码最佳实践。

正如 Andrzej Doyle 所说,当 Symetric 和 Transitive 属性分布在多个类中时,它变得难以实现。

于 2013-06-19T17:11:13.680 回答
1

Object.equals()要求在多次调用中是自反的、对称的、传递的、一致的,并且x.equals(null)必须为假。除此之外没有其他要求。

如果equals()为您定义的类做了所有这些事情,那么它是一种可接受的equals()方法。除了您自己提供的那个之外,它应该有多细的问题没有答案。你需要问自己:我希望哪些对象是平等的?

但是请注意,您应该有充分的理由让不同类的实例a.equals(b)何时a和为真b,因为这会使equals()在两个类中实现正确的变得棘手。

于 2013-06-19T17:09:46.537 回答
1

在我看来,这个问题似乎表明了一个泥泞的架构。理论上,如果你想实现 .equals 以便只比较两个实例的特定成员,你可以这样做,但这是否是一个好主意真的取决于这些类的目的是什么(即使那样我认为有更好的方法)。

这些对象或多或少只是数据包?如果是这样,也许您应该创建一个单独的比较类来确定这两个对象对于您需要的目的是否“足够等效”,而不是强迫对象本身关心一些陌生的、不相关的类。我会担心我的代码是否与可能不相关的对象有关,只是因为我认为由于暂时的方便而让他们相互了解可能是个好主意。此外,正如 Andrzej 所提到的,父类知道或关心派生类的具体实现细节是非常成问题的。我亲眼目睹了这如何导致微妙和令人震惊的问题。

对象是“执行者”而不是数据存储吗?由于您的子类 D 实现了一个方法,因此这表明它不仅仅是一袋数据......在这种情况下,从哲学上讲,我看不出仅仅基于一组值字段。比较字段,是的。认为它们相等或等价?不,从长远来看,这听起来像是一场可维护性的噩梦。

这是我认为更好的主意的示例:

class A implements IFoo{

    private int foo;
    public int getFoo(){ return foo; }

}

class B implements IFoo{

    private int foo;
    public int getFoo(){ return foo; }

}

class CompareFoos{
    public static boolean isEquivalent(IFoo a, IFoo b){
        // compare here as needed and return result.
    }
}

IFoo a = new A();
IFoo b = new B();
boolean result = CompareFoos.isEquivalent(a, b);
于 2013-06-19T17:14:11.477 回答
0

简短的回答是,在非常有限的情况下(例如IntegerLong),不同类的两个对象相等是可以的,但是很难正确地实现,因此通常不鼓励这样做。确保您创建一个SymmetricReflexiveTransitiveequals()的方法已经够难的了,但除此之外,您还应该:

  • 创建一个hashCode()equals()
  • 创建一个compareTo()equals()
  • 进一步确保该compareTo()方法表现良好

不这样做可能会导致在使用集合、树、散列和排序列表中的对象时出现问题。

有一篇很好的文章equals()基于 Josh Bloch 的《Effective Java 》一书的部分主题,但即便如此也只涵盖equals(). 这本书更详细地介绍了,特别是关于一旦开始使用集合中的对象,问题如何迅速变得严重。

于 2013-06-20T05:59:09.840 回答
0

在某种程度上,这取决于您希望代码做什么。只要您清楚equals方法将比较什么,我就不一定会看到问题。我认为当您开始制作大量子类(例如 E 类)时,问题就真的出现了。有一个危险的是其中一个子类不会遵守合同,所以你最终可能会得到

a.equals(e) --> true
e.equals(a) --> false

这会导致奇怪的行为。

就我个人而言,我尽量避免比较两个不同的类并返回 true,但我已经做了几次,整个类层次结构都在我的控制之下并最终确定。

于 2013-06-19T17:10:29.267 回答
0

考虑这个例子 -

abstract class Quadrilateral{
    Quadrilateral(int l, int w){
        length=l;
        width=w;
    }

    private int length, width;
}

class Rectangle extends Quadrilateral{
    Rectangle(int l, int w){super(l,w);}
}

class Square extends Quadrilateral{
    Square(int l){super(l, l);}
}

Square s = new Square(3);
Rectangle r = new Rectangle(3,3);

r.equals(s);//This should be true because the rectangle and square are logically the same.

所以是的,在某些情况下,两个不同的类可以相等。尽管显然这并不常见。

于 2013-06-19T17:14:20.787 回答
0

不。

Set<Parent> p = new HashSet<>();
p.insert(new Parent(1));
p.insert(new Child(1));

假设这两个实例是equals,是否p包含一个Child? 很不清楚。更多乐趣:

class Parent {
    public int foo = 0;
    void foo() {
        foo = 1;
    }
}

class Child extends Parent {
    @Override
    void foo() {
        foo = 2;
    }
}

Set<Parent> set = new HashSet<>();

for(Parent parent : set) {
    parent.foo();
    System.out.println(parent.foo); // 1 or 2?
}

我挑战您pSetHashSetequals.

于 2013-06-19T17:22:04.510 回答
-1

正如@andrzeg 所说,这取决于您需要什么。

还有一件事。您是否希望 a.equals(d) 为真,但 d.equals(a) 为假?在编码时,您可能会使用instanceof可能或可能不是您想要的东西。

于 2013-06-19T17:11:32.513 回答
-2

我认为这个问题可以归结为是否在 equals() 实现中使用 instanceof 或 getClass() 。

如果 D 派生 A 并且您的继承层次结构是正确的,那么 D 确实是 A。将 D 视为任何其他 A 是合乎逻辑的,因此您应该能够像检查任何其他 A 一样检查 D 是否相等。

这是 Josh Bloch 解释为什么他喜欢 instanceof 方法的链接。

http://www.artima.com/intv/bloch17.html

于 2013-06-19T17:31:31.610 回答