2个不同类的Java对象是否应该相等?

时间:2013-06-19 17:02:57

标签: java

我正在尝试编写一些通用代码,以根据字段列表定义类相等性和哈希码。在编写我的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这有点脏,但我欢迎对这个要点发表评论。

11 个答案:

答案 0 :(得分:21)

他们可以,但在这种情况下维护the symmetric and transitive properties of equality通常非常困难。至少在有一个有用/直观的平等定义时。

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

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

或者,您在C中覆盖,如果另一个对象是bar的实例,则比较C,但对A的实例不进行比较,你有另一个问题。 c1.equals(c2)将是错误的,但c1.equals(a)将是真的,c2.equals(a)也是如此a.equals(c2)。这打破了传递性(因为c1 == aa == c2暗示c1 == c2)。


总之,理论上可行,但你必须削弱你的平等实现才能这样做。此外,运行时类是一个对象的属性,就像bar一样,所以我希望具有不同具体类的对象彼此不相等无论如何。 / p>

答案 1 :(得分:4)

首先注意:当您覆盖.equals()时,您绝对必须覆盖.hashCode(),并遵守defined contract。这不是开玩笑。如果你不遵守这一点,你注定会遇到问题。

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

@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
}

答案 2 :(得分:1)

Object.equals()必须在多个调用中具有反身性,对称性,传递性,一致性,并且x.equals(null)必须为false。除此之外没有其他要求。

如果您定义的类的equals()执行了所有这些操作,则它是可接受的equals()方法。除了你自己提供的那个问题之外,它应该是多么精细的问题没有答案。你需要问自己:我想要哪些对象是平等的?

但是,请注意,当a.equals(b)a是不同类的实例时,您应该有充分的理由使b为true,因为这可能会使实现正确的方法变得棘手两个班级都equals()

答案 3 :(得分: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进行比较必须为false,并且不应导致NullPointerException。例如,a.equals(null)必须为false,将未知对象(可能为null)传递给Java中的equals实际上是Java编码最佳实践,以避免Java中的NullPointerException。
  6. Andrzej Doyle正确地说,当它跨越多个类时,很难实现Symetric和Transitive属性。

答案 4 :(得分:1)

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

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

对象是“行动者”而不是数据存储?既然你的子类D实现了一个方法,那么这表明它不仅仅是一个数据包...在这种情况下,从哲学上讲,我看不出如何考虑A和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);

答案 5 :(得分:0)

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

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

会导致奇怪的行为。

就个人而言,我试图避免等于比较两个不同的类并返回true但我已经完成了几次整个类层次结构在我的控制和最终之下。

答案 6 :(得分: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.

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

答案 7 :(得分:0)

没有

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

假设这两个实例是equalsp是否包含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?
}

我要求您了解p的元素包含的内容,而不会在SetHashSetequals的Javadoc页面上花费超过1分钟。

答案 8 :(得分:0)

简短的回答是,在非常有限的情况下(例如IntegerLong),不同类的两个对象可以相等,但很难正确地完成它通常气馁。确保您创建一个{em> Symmetric , Reflexive Transitive equals()方法是非常困难的,但最重要的是你还应该:

  • 创建与hashCode()
  • 一致的equals()方法
  • 创建与compareTo()
  • 一致的equals()方法
  • 进一步确保compareTo()方法为well behaved

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

根据Josh Bloch的书 Effective Java 的一部分,equals()的主题有一个good article,但即使只涵盖equals()。本书详细介绍了一下,特别是一旦开始使用集合中的对象,问题就会迅速变得严重。

答案 9 :(得分:-1)

正如@andrzeg所说,这取决于你的需要。

另外一件事。你想a.equals(d)是真的,但是d.equals(a)是假的吗?在编码时,您可能会使用instanceof,这可能是您想要的,也可能不是。

答案 10 :(得分:-2)

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

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

这是一个链接,Josh Bloch解释了为什么他喜欢这种方法的实例。

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