Должен ли я переопределять equals и hashCode в дочерних классах, даже если он ничего не добавляет? - PullRequest
0 голосов
/ 01 февраля 2019

У меня есть абстрактный класс, который переопределяет equals() и hashCode().В этих методах я использую абстрактный метод getDescription() для проверки на равенство и для генерации hashCode.Теперь, когда я расширяю класс и добавляю поле, которое используется только в методе getDescription(), я получаю проблему sonarLint «Расширяет класс, который переопределяет equals и добавляет поля».Это просто сонар недостаточно развит, чтобы понять, что происходит, или я делаю это не Java, и есть лучший / более элегантный способ?

Родительский класс:

public abstract String getDescription();   

@Override
public int hashCode()
{
    return new HashCodeBuilder(19, 71).
            append(mViolation).
            append(getDescription()).
            append(mProperties).
            toHashCode();
}

@Override
public boolean equals(
    final Object obj)
{
    boolean equal = false;
    if (this == obj)
    {
        equal = true;
    }
    else if (obj instanceof parent)
    {
        AbstractStructuredDataIssue rhs = (parent) obj;
        equal = new EqualsBuilder().
                append(mViolation, rhs.mViolation).
                append(getDescription(), rhs.getDescription()).
                append(mProperties, rhs.mProperties).
                isEquals();
    }
    return equal;
}

Детский класс:

public class Child extends Parent {
    private final String mMessage;

    public Child(final String message, final int number) {
        super(number);
        mMessage = message;
    }

    @Override
    public String getDescription()
    {
        return String.format(
                DESCRIPTION_FORMAT,
                mMessage);
    }
}

Ответы [ 4 ]

0 голосов
/ 01 февраля 2019

В вашей реализации вы можете иметь две Parent ссылки, которые равны , и все же указывают на объекты двух разных классов, так что один может быть приведен к Child, а другой - нет.

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

0 голосов
/ 01 февраля 2019

Давайте посмотрим на правило RSPEC-2160 :

Расширьте класс, который переопределяет equals, и добавьте поля без переопределения equals в подклассе, и вы рискуетеиз неэквивалентных экземпляров вашего подкласса , рассматриваемых как равные , поскольку только поля суперкласса будут рассматриваться в тесте на равенство .

Сонар указывает на риск того, что вы получите неравных объектов, которые будут рассматриваться как равные , поскольку при вызове equals в вашем подклассе, без надлежащей реализации, толькоБудут оцениваться поля родительского класса.

Пример кода, не соответствующего требованиям (из документов)

public class Fruit {
  private Season ripe;

  public boolean equals(Object obj) {
    if (obj == this) {
      return true;
    }
    if (this.class != obj.class) {
      return false;
    }
    Fruit fobj = (Fruit) obj;
    if (ripe.equals(fobj.getRipe()) {
      return true;
    }
    return false;
  }
}

public class Raspberry extends Fruit {  // Noncompliant; instances will use Fruit's equals method
  private Color ripeColor;
}

Совместимое решение (также из документов)

public class Fruit {
  private Season ripe;

  public boolean equals(Object obj) {
    if (obj == this) {
      return true;
    }
    if (this.class != obj.class) {
      return false;
    }
    Fruit fobj = (Fruit) obj;
    if (ripe.equals(fobj.getRipe()) {
      return true;
    }
    return false;
  }
}

public class Raspberry extends Fruit {
  private Color ripeColor;

  public boolean equals(Object obj) {
    if (! super.equals(obj)) {
      return false;
    }
    Raspberry fobj = (Raspberry) obj;
    if (ripeColor.equals(fobj.getRipeColor()) {  // added fields are tested
      return true;
    }
    return false;
  }
}

Я согласен с вашей точкой зрения, что Sonar может быть недостаточно изощренным, чтобы видеть, что происходит во время выполнения, поэтому он указывает на запах кода.

Вам нужно беспокоиться, чтобы не сломатьequals и hashCode контракт, вашПодход динамический, который, вероятно, не рассматривается Sonar.

0 голосов
/ 01 февраля 2019
  • Переопределение методов equals () и hashcode () в дочернем классе будет эффективным с учетом дочерних элементов (переменных) класса, а также помогает при использовании подтипов структуры Collection и экземпляров Map для поиска нужной памятипробел (интервал) во время операций каркаса коллекции (например, сохранить / получить).

    Наследование от суперкласса может пропустить элементы дочернего класса для эффективной генерации функциональности хэш-кода / метода равно.

0 голосов
/ 01 февраля 2019

Это немного сложно;Я должен объяснить несколько вещей о том, как работает метод equals и hashCode для объяснения жизнеспособных решений.

Существует «контракт».Компилятор не может применить его, но если вы не будете придерживаться этого контракта, произойдут странные вещи.В частности: Ваши объекты будут просто делать что-то не то, когда используются в качестве ключей в хэш-картах, и, возможно, другие подобные проблемы при использовании сторонних библиотек.Чтобы должным образом придерживаться контракта, любой данный класс либо должен полностью отказаться от equals / hashCode, либо OR, всей цепочке (таким образом, класс и все его подклассы) должны корректно переопределить hashCode и equals, за исключением того, что вы действительно можете 'не делайте этого, если только родитель не получает соответствующих инструкций.

В договоре указано, что это всегда должно быть правильно:

  • a.equals (b) -> b.equals (a).
  • a.equals (b) и b.equals (c) -> a.equals (c).
  • a.equals (a).
  • a.equals (b) -> a.hashCode () == b.hashCode ().(обратите внимание, что обратное не обязательно должно быть правдой; одинаковые хеш-коды не означают, что объекты равны).

Контракт ДЕЙСТВИТЕЛЬНО трудно гарантировать перед лицом иерархии классов!Представьте, что мы берем существующий java.util.ArrayList и создаем его подкласс с понятием «цвет».Так что теперь у нас может быть синий ColoredArrayList или красный ColoredArrayList.Было бы разумно сказать, что синий ColoredArrayList определенно НЕ должен равняться красному ColoredArrayList, за исключением того, что ... равно значению самого ArrayList (который вы не можете изменить), фактически определяет, что вы просто не можете расширять ArrayList с помощью свойств, подобных этому вообще : если вы вызываете a.equals (b), где a - пустой массив, а b - какой-то пустой список (скажем, пустой красный ColoredArrayList), он просто проверит равенство каждого члена в нем, что,учитывая, что они оба пусты, это тривиально верно.Таким образом, пустой нормальный массив ArrayList равен как пустому красному, так и пустому синему ColoredArrayList, и поэтому в контракте предусматривалось, что пустой красный ColoredArrayList должен совпадать с пустым синим ColoredArrayList.В этом смысле сонар здесь просто сломан.Есть проблема, и она не решаема. Невозможно написать концепцию ColoredArrayList в java .

Тем не менее, решение есть, но только если каждый класс в иерархии находится на борту.Это canEqual подход.Выход из цветной дилеммы, описанный выше, состоит в том, чтобы дифференцировать понятия «Я расширяю и добавляю новые свойства» и «Я расширяю, но эти вещи все еще семантически говорят о том же, что и без новых свойств».ColoredArrayList - первый случай: это расширение, которое добавляет новые свойства.Идея canEqual заключается в том, что вы создаете отдельный метод для указания этого, что позволяет ArrayList выяснить: я не могу быть равен ЛЮБОМУ экземпляру ColoredArrayList, даже если все элементы одинаковы.Тогда мы можем снова придерживаться договора.ArrayList НЕ имеет этой системы на месте, и, следовательно, учитывая, что вы не можете изменить исходный код ArrayList, вы застряли: это невозможно исправить.Но если вы напишите свою собственную иерархию классов, вы можете добавить ее.

Project Lombok позаботится о том, чтобы добавить для вас equals и hashCode.Даже если вы не хотите его использовать, вы можете посмотреть, что он генерирует, и продублировать его в своем собственном коде.Это также удалит предупреждения, которые испускает сонар.См. https://projectlombok.org/features/EqualsAndHashCode - здесь также показано, как можно использовать концепцию canEqual, чтобы избежать дилеммы ColoredArrayList.

Здесь вы создаете подкласс без добавления новых свойств, поэтому нет необходимости заменять hashCodeи равно.Но сонар этого не знает.

...