繁体   English   中英

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

[英]Should 2 Java objects of different classes ever be equal?

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

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

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

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

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


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

首先注意:当你覆盖.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
}

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

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

但是请注意,当ab是不同类的实例时,你应该有充分的理由使a.equals(b) true,因为这样可以在两个类中实现正确的equals()变得棘手。

还要记住,您需要遵循这些规则才能正确实现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。

正如Andrzej Doyle所说,当它跨越多个类时,实现Symetric和Transitive属性变得很困难。

在我看来,这个问题表明了一个泥泞的架构。 从理论上讲,如果你想实现.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);

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

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

这会导致奇怪的行为。

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

考虑这个例子 -

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.

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

没有。

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分钟。

简短的回答是,在非常有限的情况下(例如IntegerLong Integer ),两个不同类的对象可以相等,但是很难正确地推断它通常是不鼓励的。 确保创建一个对称自反传递equals()方法是很困难的,但除此之外,您还应该:

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

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

一篇关于equals()主题的好文章基于Josh Bloch的书“ Effective Java”的一部分,但即便只涉及equals() 本书详细介绍了一下,特别是在开始使用集合中的对象后,问题会如何迅速变得严重。

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM