Реализация equals (): сравнить с реализованным интерфейсом или классом реализации? - PullRequest
8 голосов
/ 12 августа 2011

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

Я не подготовил свой конкретный пример, но в JDK есть два примера - java.lang.Number и java.lang.CharSequence, которые иллюстрируют решение:

boolean b1 = new Byte(0).equals( new Integer(0) ) );

или с CharSequence

boolean b2 = "".equals(new StringBuilder());

Хотели бы вы, чтобы те оценивали как истинные или нет? Оба типа реализуют один и тот же интерфейс типа данных, и в качестве клиента, работающего с экземплярами Numbers (или CharSequence), у меня было бы проще, если бы equals сравнивал типы интерфейса вместо реализующих типов.

Теперь это не идеальный пример, поскольку JDK предоставляет открытые типы реализации, но предположим, что нам не нужно было поддерживать совместимость с тем, что уже есть - с точки зрения проектировщиков: Следует равняется проверке по интерфейсу или лучше так, как проверяет по реализации?


Примечание: я понимаю, что проверка на равенство с интерфейсом может быть очень трудной для правильной реализации на практике, и это даже делает более хитрым , поскольку равные интерфейсы также должны возвращать тот же hashCode (). Но это только препятствия на пути реализации, например, CharSequence, хотя интерфейс довольно мал, все необходимое для проверки на равенство присутствует без раскрытия внутренней структуры реализации (поэтому принципиально возможно реализовать правильно, даже без заранее зная о будущих реализациях). Но меня больше интересует аспект design , а не то, как на самом деле его реализовать. Я бы не решил, основываясь исключительно на том, как сложно что-то реализовать.

Ответы [ 6 ]

5 голосов
/ 12 августа 2011

Определите абстрактный класс, который implements ваш интерфейс, и определите методы final equals () / hashCode (), и пусть ваши клиенты расширят его:

public interface Somethingable {
    public void something();
}

public abstract class AbstractSomethingable implements Somethingable {
    public final boolean equals(Object obj) {
        // your consistent implementation
    }

    public final int hashCode() {
        // your consistent implementation
    }
}

Обратите внимание, что, сделав ваш класс abstract, вы можете implements интерфейс без определения методов интерфейса.

Ваши клиенты по-прежнему должны реализовать метод something(), но все их экземпляры будут использовать ваш код для equals () / hashCode () (поскольку вы создали эти методы final).

Разница для ваших клиентов:

  • Использование ключевого слова extends вместо ключевого слова implements (несовершеннолетний)
  • Не имея возможности расширить какой-либо другой класс по своему выбору для использования вашего API (может быть второстепенным, может быть основным - если это приемлемо, тогда пойти на это)
3 голосов
/ 12 августа 2011

Обычно я предполагаю, что «похожие» объекты не будут равны - например, я не ожидал бы, что Integer (1) пройдет равно (Long (1)). Я могу представить себе ситуации, в которых это было бы разумно, но поскольку jdk должен быть универсальным API, вы не сможете сделать предположение, что это всегда будет правильным решением. 1003 *

Если у вас есть какие-то пользовательские объекты, где это разумно, я думаю, что было бы прекрасно реализовать расширенное определение равенства, если вы

  • уверены, что у вас нет некоторых крайних случаев, когда вам действительно нужно более конкретное равенство (т. Е. Для этого потребуются идентичные классы)
  • документ это очень четко
  • убедитесь, что хэш-код работает в соответствии с вашими новыми равными.
0 голосов
/ 12 августа 2011

Можно определить равенство между различными классами.

В вашем случае интерфейс точного равенства должен быть указан интерфейсом, поэтому любой класс, реализующий интерфейс, должен его соблюдать.А еще лучше, поскольку алгоритм зависит только от информации, предоставляемой интерфейсом, просто реализуйте его, поэтому подклассы могут просто заимствовать его.

interface Foo

class Util 

    static int hashCode(Foo foo){ ... }

    static boolean equal(Foo a, Foo b){ ... }

    static boolean equal(Foo a, Object b)
        return (b instanceof Foo) && equal(a, (Foo)b);

class FooX implements Foo

    int hashCode()
        return Util.hashCode(this); 

    boolean equals(Object that)
        return Util.equal(this, that);
0 голосов
/ 12 августа 2011

Я бы попробовал добавить другой метод equals в мой интерфейс. Как насчет этого:

assertFalse(new Integer(0).equals(new Byte(0))); // pass
assertTrue(new Integer(0).valueEquals(new Byte(0))); // hypothetical pass

Это не приводит к неожиданному поведению (разные типы равны), но оставляет возможность открытой для проверки на равные значения.

В эффективной Java есть несколько взаимосвязанная тема, в которой обсуждается равенство с instanceof и getClass. Хотя не могу вспомнить номер предмета.

0 голосов
/ 12 августа 2011

Я бы посчитал, что любая реализация equals, которая возвращает true для двух объектов, которые не имеют один и тот же конкретный тип, является чрезвычайно «удивительным» поведением. Если вы работаете внутри блока, в котором вы знаете во время компиляции всех возможных разработчиков интерфейса, вы можете изготовить выражения, которые имеют смысл, только с помощью методов интерфейса, но это не является реальностью для кода API / framework.

Вы даже не можете быть уверены, что никто не собирается писать реализацию интерфейса, который изменяет свое внутреннее состояние, когда вы вызываете методы, которые вы использовали для реализации, равны! Поговорите о путанице, проверке равенства, которая возвращает истину и делает себя недействительной в процессе?

-

Это то, что я понял как вопрос «проверки равенства по интерфейсу»:

public interface Car {

  int speedKMH();
  String directionCardinal();

}

public class BoringCorrolla implements Car {

  private int speed;
  private String directionCardinal;

  public int speedKMH() { return speed; }
  public String directionCardinal() { return directionCardinal; }

  @Override
  public boolean equals(Object obj) {
    if (obj isntanceof Car) {
      Car other = (Car) obj;
      return (other.speedKMH() == speedKMH() && other.directionCardinal().equals(directionCardinal());
    }
  }
}

public class CRAZYTAXI implements Car, RandomCar {

      public int speedKMH() { return randomSpeed(); }
      public String directionCardinal() { return randomDirection();}
    }
0 голосов
/ 12 августа 2011

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

Часто уровень реализации работает нормально, так как ваш клиент имеет дело только с одной реализацией (т. Е. MyNumberProcessor может работает с любым номером, но практически один его экземпляр должен обрабатывать только Long, а может быть только другой Double ). Дженерики - отличный способ убедиться, что это происходит.

В том редком случае, когда это имеет значение, я бы, вероятно, разработал клиент так, чтобы он допускал внедрение Comparator или - когда он недоступен - инкапсулировал мои Numbers в VarTypeNumber.

...