переопределение метода equals при работе с наследованием

Я читал о том, как лучше всего переопределить метод equals при работе с подклассами, и здесь я нашел довольно много сообщений. Они рекомендуют разные способы реализации решения с использованием instanceof или getClass() для сравнения объектов разных подклассов.

Однако со ссылкой на Effective Java я понимаю (и я новичок в этом, поэтому я вполне могу ошибаться!) Блох утверждает, что в конечном итоге и то, и другое может быть проблематичным: «Нет способа расширить инстанцируемый класс и добавить значение компонента при сохранении контракта equals, если только вы не готовы отказаться от преимуществ объектно-ориентированной абстракции». Затем рекомендует «предпочесть композицию наследованию».

Итак, я имею дело с этой иерархией классов: AbstractClass, ConcreteClass1 и ConcreteClass2. ConcreteClass1 расширяет AbstractClass, а ConcreteClass2 расширяет ConcreteClass1. На данный момент только AbstractClass переопределяет метод equals.

Итак, в AbstractClass:

public abstract class AbstractClass {
        private String id;


        public boolean equals(Object other) {
            return other != null && other.getClass().equals(getClass())
                    && id.equals(((AbstractClass) other).id);
        }

    }

А в ConcreteClass1 у меня есть:

public class ConcreteClassOne extends AbstractClass
{
  private final AbstractClass parent;

  public ConcreteClassOne( String anId, AbstractClass aParent )
  {
    super( anId );

    parent = aParent;
  }

}

Наконец, в ConcreteClassTwo у меня есть:

public class ConcreteClassTwo extends ConcreteClassOne
{
  private static int nextTrackingNo = 0;

  private final int trackingNo;

  public ConcreteClassTwo ( String anId )
  {
    super( anId, null );

    trackingNo= getNextTrackingNo();
  }
}

Поэтому я считаю, что мне нужно переопределить метод equals как в ConcreteClassOne, так и в ConcreteClassTwo, чтобы включить важные поля parent и trackingNo. Мне не разрешено менять дизайн, поэтому использовать композицию нельзя. Какие-либо предложения?


person user2397607    schedule 21.05.2013    source источник
comment
Итак... В чем вопрос/проблема? Нет ничего плохого в переопределении метода equals.   -  person Supericy    schedule 21.05.2013
comment
Вопрос в том, чтобы переопределить equals при соблюдении его контракта.   -  person Marko Topolnik    schedule 21.05.2013
comment
@MarkoTopolnik, ты меня понял!   -  person user2397607    schedule 21.05.2013
comment
К вашему сведению: Анжелика Лангер — Секреты равных   -  person McDowell    schedule 21.05.2013
comment
@McDowell спасибо, я наткнулся на это, но, честно говоря, до сих пор не имел возможности полностью его изучить.   -  person user2397607    schedule 21.05.2013


Ответы (3)


Самый простой подход — расширить equals() как в конкретном, так и в абстрактном классах.

public class ConcreteClassTwo extends ConcreteClassOne {
    public boolean equals(Object other) {
        boolean rv = super.equals( other );
        if ( other instanceof ConcreteClassTwo ) {
           rv = rv && (this.trackingNo == ((ConcreteClassTwo) other).trackingNo);
        }
        return rv;
    }
}
person Andy Thomas    schedule 21.05.2013
comment
Спасибо! А что касается instanceof и getClass(), правильно ли я говорю, что безопаснее придерживаться getClass()? то есть объекты двух разных подклассов никогда не должны быть одинаковыми. скажем, если я расширим ConcreteClass1, чтобы иметь ConcreteClass3, объекты, созданные из CC2 и CC3, не будут равны, даже если они имеют одинаковое содержимое. - person user2397607; 21.05.2013
comment
@ user2397607 - В вашем случае да. В общем, Блох предпочитает instanceof методу getClass() для поддержки тривиальных подклассов, не добавляющих никакого состояния. Но здесь рефлексивность удовлетворяется вашей реализацией AbstractClass.equals() и моей реализацией ConcreteClassTwo.equals(). С ними c1.equals(c2) == c2.equals(c1). - person Andy Thomas; 21.05.2013
comment
Но вы нарушаете транзитивность, так что здесь ничего особенного не выиграете. - person Marko Topolnik; 21.05.2013
comment
@MarkoTopolnik, так является ли композиция Блоха по наследованию окончательным последним решением? то есть в его нынешнем виде это проблема дизайна, а не реализации. - person user2397607; 21.05.2013
comment
@MarkoTopolnik - я исправлял ваш предыдущий комментарий к этому ответу. Можете ли вы привести пример проблемы транзитивности, которую вы видите? [Изменить: добавлен отсутствующий оператор возврата.] - person Andy Thomas; 21.05.2013
comment
@AndyThomas-Cramer заметил это :) - person user2397607; 21.05.2013
comment
Если у вас есть c2a, c1 и c2b (экземпляры ConcreteClassTwo, One и Two соответственно), вы можете иметь c2a.equals(c1) и c1.equals(c2b), не гарантируя c2a.equals(c2b). - person Marko Topolnik; 22.05.2013
comment
Вы, кажется, ошибаетесь. См. ideone.com/3yMrU8. Тем не менее, я бы предпочел (чтобы) избежать этой проблемы, сделав нелистовые классы abstact; в противном случае избегайте добавления компонента значения в подкласс конкретного класса; в противном случае используйте getClass() вместо instanceof во всей иерархии классов. - person Andy Thomas; 22.05.2013

Если у вас есть equals и в ConcreteClassOne, и в ConcreteClassTwo, то симметрия equals нарушена:

Object c1 = new ConcreteClassOne(),
       c2 = new ConcreteClassTwo();
System.out.println("c1=c2? " + c1.equals(c2)");
System.out.println("c2=c1? " + c2.equals(c1)");

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

true
false

потому что в c2.equals у вас есть instanceof ConcreteClassTwo, который не проходит для c1, а в обратном случае аналогичная проверка проходит.

person Marko Topolnik    schedule 21.05.2013
comment
Да, я думал, что вам нужно дополнительное объяснение/подтверждение этого момента. Блох прав, и вы ничего не можете сделать, кроме как допустить равенство только при условии a.getClass()==b.getClass(). - person Marko Topolnik; 21.05.2013
comment
Да, как я уже упоминал, я новичок в этой концепции, поэтому приветствуется подтверждение того, что я понял с точки зрения Блоха. - person user2397607; 21.05.2013

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

Примером последней ситуации может быть базовый класс ImmutableSquareFloatMatrix с методами int GetSize() и float GetCell(int row, int column). Обычная реализация будет иметь массив значений с плавающей запятой (размер * размер), но можно также иметь, например. классы ZeroMatrix и IdentityMatrix, единственное поле которых указывает размер, класс ConstantMatrix с полем, определяющим размер, и поле, определяющее значение, которое должно быть возвращено для каждой ячейки, класс DiagonalMatrix с одномерным массивом, содержащим только элементы для диагонали ( метод GetCell вернет ноль для всего остального) и т. д.

Имея два экземпляра классов, производных от ImmutableSquareFloatMatrix, можно было бы сравнить их, сравнив их размеры, а затем сравнив все содержащиеся в них значения, но во многих случаях это было бы неэффективно. Если один из сравниваемых объектов «знает» о типе другого объекта, можно значительно повысить эффективность. Если ни один объект не знает о другом, возврат к методу сравнения по умолчанию может быть медленным, но в любом случае даст правильные результаты.

Рабочим подходом для обработки этой ситуации может быть реализация базовым типом метода equals2, который возвращает 1, если его специальные знания о другом объекте означают, что он может сказать, что он равен, -1, если он может сказать, что он не равен, или 0, если бы он не мог сказать. Если метод equals2 любого типа знает, что они неравны, они неравны. В противном случае, если кто-либо знает, что они равны, они равны. В противном случае проверьте равенство, используя сравнение ячеек за ячейками.

person supercat    schedule 21.05.2013