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

Что пойдет не так, когда суперкласс не переопределит equals() и hashCode()?

Предположим, что существуют два класса:

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 */
}

Затем я сгенерирую методы equals() и hashCode() в классе B с помощью Eclipse "Source → Generate hashCode() и equals()...". Но Eclipse предупреждает меня, что:

Суперкласс 'com.example.test2.A' не переопределяет equals() и hashCode() - полученный код может работать некорректно.

Итак, , что приведет к тому, что полученный код не будет корректно работать с сгенерированными методами?


(BTW, сгенерированные методы выглядят так:

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

)

4b9b3361

Ответ 1

Вы должны быть осторожны, если переопределить равны, чтобы придерживаться определенного набора правил. Подробнее см. javadoc. Короче говоря, две сложные части сохраняют симметрию и транзитивность. Согласно Джошуа Блоку Эффективная Java

"Невозможно расширить экземпляр класса и добавить компонент значения при сохранении равного контракта"

Что это значит? Хорошо, предположим, что у вас в классе A есть свойство типа T и в подклассе B другое свойство типа V. Если оба класса переопределяют равны, тогда вы получите разные результаты при сравнении 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 от Object и будут сравниваться на основе идентификации объекта (то есть адреса памяти). Это может привести к несимметричному сравнению, если используется с подклассом, который делает переопределение равным.

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

Ответ 2

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

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

Имейте в виду, что любой получувствительный метод equals будет иметь проверку типа. Это верно для автоматически сгенерированного кода в исходном вопросе:

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

Вместо того, чтобы просто процитировать куски Блохи книги и других веб-сайтов, мы должны думать долго и упорно о том, какой-либо из "стандартных" равно возможны проблемы, если родительский класс не реализует равные. Я бы сказал, что это не так, и я приветствую встречный пример.

Ответ 3

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

Проблемная ситуация возникает, когда возможно иметь экземпляры, которые имеют разные классы, но, тем не менее, должны сравниваться равными. Единственный правильный способ справиться с этой ситуацией - указать, что любые экземпляры, которые могут сравниваться друг с другом, должны вытекать из общего класса, а контракт для этого класса должен указывать, что означает равенство. Общий класс equals (который может быть или не быть абстрактным) обычно определяет виртуальных членов, которые производные классы могут переопределять для проверки различных аспектов равенства. Если, например, метод common-class equals имеет значение if (other==null) return false; else return this.equals2(other) && other.equals2(this);, это по существу гарантирует симметричное поведение для любой реализации equals2, которая не мутирует сравниваемые объекты.

Ответ 4

Подождав время для убедительной причины, я беру.

Связанная мной статья (опять же, http://www.artima.com/lejava/articles/equality.html) объясняет, почему возникают проблемы при реализации равных в подклассе (в основном, что проверка instanceof приводит к асимметриям, которые запрещены контрактом equals).

Если ваш суперкласс не реализует равные, вы можете найти эту ситуацию.

  • B расширяет A

  • B реализует equals, скажем, как

    public boolean equals(Object obj) {
      A a = (A) obj;  <-- this cast is the most problematic issue from this example
      return this.id.equals(a.getId());
    }
    

Итак, вы заканчиваете с

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

Один из пунктов, которые будут противоречить этому примеру, заключается в том, что ваш суперкласс является абстрактным, но, вероятно, Eclipse выдает предупреждение в предупреждении о том, что вы его создаете (или это только проверка предупреждения не настолько тонкая).