Подтвердить что ты не робот

Должно ли два 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 Это немного грязно, но я бы приветствовал комментарии суть.

4b9b3361

Ответ 1

Они могут быть, но обычно очень сложно поддерживать симметричные и транзитивные свойства равенства в этом случае. По крайней мере, имея полезное/интуитивное определение равенства.

Если вы разрешаете подклассу считать себя равным экземпляру суперкласса, тогда суперкласс должен считать себя равным экземпляру подкласса. Это означает, что вы будете кодировать определенные знания о подклассе (все возможные подклассы?) В суперклассе и при необходимости уменьшать вниз, что не очень чисто.

Или вы выполняете сравнение чисто с полями, содержащимися в A, и не переопределяете equals() вообще. Это исправляет вышеуказанное, но имеет проблему, что два экземпляра C с разными значениями bar считаются равными, что, вероятно, не так, как вы хотите.

Или вы переопределите в C и сравните bar, если другой объект является экземпляром C, но в противном случае не для экземпляра A, у вас есть другая проблема. c1.equals(c2) будет ложным, но c1.equals(a) будет истинным, как и c2.equals(a), и поэтому a.equals(c2). Это нарушает транзитивность (поскольку c1 == a и a == c2 влечет c1 == c2).


Таким образом, теоретически это возможно, но для этого вам придется калечить реализацию equals. Кроме того, класс runtime является свойством объекта, равным целям bar, поэтому я ожидал бы, что объекты с разными конкретными классами будут в любом случае не равными друг другу.

Ответ 2

Первое примечание: когда вы переопределяете .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
}

Ответ 3

Object.equals() требуется рефлексивный, симметричный, транзитивный, согласованный между несколькими вызовами, а x.equals(null) должен быть ложным. Дальнейших требований нет.

Если equals() для класса, который вы определяете, выполняет все эти вещи, то это приемлемый метод equals(). Нет ответа на вопрос о том, как мелкозернистый он должен отличаться от того, который вы предоставляете себе. Вы должны спросить себя: какие объекты я хочу быть равными?

Обратите внимание, однако, что у вас должна быть веская причина сделать a.equals(b) true, когда a и b являются экземплярами разных классов, поскольку это может затруднить реализацию правильного equals() в обоих классах.

Ответ 4

Также помните, что вам нужно следовать этим правилам, чтобы правильно реализовать метод equals.

  • Рефлексивный: объект должен быть равен самому себе.
  • Симметричный: если a.equals(b) истинно, то b.равнения (a) должны быть истинными.
  • Transitive: если a.equals(b) истинно, а b.equals(c) истинно, то c.equals(a) должно быть истинным.
  • Согласованность: множественное обращение метода equals() должно приводить к одному значению до тех пор, пока какое-либо из свойств не будет изменено. Поэтому, если два объекта равны в Java, они останутся равными до тех пор, пока какое-либо из свойств не будет изменено.
  • Нулевое сравнение: сравнение любого объекта с нулевым значением должно быть ложным и не должно приводить к исключению NullPointerException. Например, a.equals(null) должно быть ложным, передача неизвестного объекта, который может быть null, равным в Java, на самом деле является лучшей практикой Java-кодирования, чтобы избежать NullPointerException в Java.

Как справедливо сказал Анджей Дойл, становится трудно реализовать свойство Symetric и Transitive, когда оно распространяется на несколько классов.

Ответ 5

Этот вопрос, кажется, указывает на грязную архитектуру. Теоретически, если вы хотите реализовать .equals, чтобы вы сравнивали только определенные члены этих двух экземпляров, вы можете это сделать, но действительно ли это хорошая идея, действительно зависит от того, для какой цели предназначены эти классы (и даже тогда я подумайте, что есть лучшие подходы).

Являются ли эти объекты более или менее просто предназначенными не что иное, как мешки с данными? Если это так, возможно, вам следует создать отдельный класс сравнения, который определяет, являются ли эти два объекта "эквивалентными" для целей, которые вам нужны, а не заставляют сами объекты заботиться о каком-то чуждом, несвязаном классе. Я был бы обеспокоен, если бы мой код касался себя потенциально несвязанными объектами только потому, что я думал, что это может быть хорошей идеей для них узнать друг о друге из-за временного удобства. Кроме того, как отметил Анджей, для родительского класса очень проблематично знать или заботиться о конкретных деталях реализации производных классов. Я видел из первых рук, как это вызывает проблемы как тонкие, так и вопиющие.

Являются ли объекты "предпринимателями", а не хранилищем данных? Поскольку ваш подкласс 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);

Ответ 6

В какой-то степени это зависит от того, что вы хотите сделать. Пока вы поняли, что будет сравнивать метод equals, я не вижу проблемы обязательно. Я думаю, что проблема действительно возникает, когда вы начинаете делать много подклассов (например, класс E). Существует опасность, что один из подклассов не будет следовать контракту, чтобы вы могли

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

что приведет к странному поведению.

Лично я стараюсь избегать сравнения двух разных классов и возврата true, но я сделал это несколько раз, когда вся иерархия классов была под моим контролем и окончательной.

Ответ 7

Рассмотрим этот пример -

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.

Итак, есть случаи, когда два разных класса могут быть равны. Хотя это явно не так.

Ответ 8

Нет.

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

Я предлагаю вам знать, что содержит элемент p, не тратя более 1 минуты на страницы Javadoc для Set, HashSet или equals.

Ответ 9

Короткий ответ заключается в том, что в очень ограниченных случаях (например, Integer и Long) нормально для двух объектов разных классов быть одинаковым, но так трудно правильно отследить, что это обычно обескураживает. Это достаточно сложно, чтобы создать метод equals(), который является симметричным, рефлексивным и переходным, но, кроме того, вы также должны:

  • создайте метод hashCode(), соответствующий equals()
  • создайте метод compareTo(), соответствующий equals()
  • далее убедитесь, что метод compareTo() хорошо себя ведет

Невозможность сделать это может вызвать проблемы с использованием объектов в наборах, деревьях, хешах и отсортированных списках.

Там хорошая статья по теме equals() на основе части книги Джоша Блоха Эффективная Java, но даже она охватывает только equals(). Книга идет гораздо более подробно, особенно о том, как проблема становится серьезной быстро после того, как вы начнете использовать объекты в коллекциях.

Ответ 10

Это зависит от того, что вам нужно, как сказал @andrzeg.

Еще одна вещь. Вы хотите, чтобы a.equals(d) были истинными, но d.equals(a) были ложными? при кодировании вы, вероятно, будете использовать instanceof, который может быть или не быть тем, что вы хотите.

Ответ 11

Я думаю, что этот вопрос может быть сведен к тому, следует ли использовать instanceof или getClass() в реализации equals().

Если D выводит A, а ваша иерархия наследования правильная, то действительно D IS A. Логично рассматривать D как любой другой A, и поэтому вы должны иметь возможность проверять D на равенство, как и на любой другой A.

Вот ссылка, где Джош Блох объясняет, почему он предпочитает подход, основанный на экземпляре.

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