Рубрики
Без рубрики

Вступает в силу со вторника! Подчиняться контракт `равных`

Погружение в десятую главу эффективной Java. Помеченный как java, эффективный, равный, архитектура.

Сегодня мы начинаем новую главу. В этой новой главе рассматриваются методы, общие для всех объектов. И какие методы являются общими для всех объектов? Ну, поскольку каждый объект в конечном итоге наследуется от Объект это были бы методы для этого объекта. Метод, о котором мы с удовольствием поговорим сегодня, – это метод equals .

Метод equals кажется простым для переопределения, но на самом деле с ним легко столкнуться с проблемами, поскольку он эффективен в деталях Java. Так почему же мы должны переопределять метод equals ? Ну, на самом деле нам и не нужно этого делать. Если ваш класс удовлетворяет любому из следующих требований, нет необходимости переопределять метод equals :

  • Каждый экземпляр класса по своей сути уникален. Thread например, не имеет смысла, чтобы два экземпляра были равны.
  • Если нет желания сравнивать два объекта одного типа. Это действительно кажется рискованным, поскольку, если другие используют ваш класс, вы не можете быть уверены, захотят ли они когда-нибудь сравнить два экземпляра класса.
  • Суперкласс реализует метод equals , который подходит для данного класса. Примерами этого являются реализация equals в AbstractList таким образом, в нет необходимости ArrayList .
  • Если класс является закрытым или закрытым для пакета, и вы можете быть уверены, что equals никогда не будет вызываться в классе.
  • Если класс использует управление экземпляром. Это что-то вроде одноэлементной модели, о которой мы говорили в предыдущем пункте, или перечислений, где существует только один экземпляр. В этих случаях логическое равенство совпадает с равенством экземпляра.

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

  • Рефлексивный : Объект должен быть равен самому себе ( x.equals(x) )

  • Симметричный : Если x.равно(y) , тогда y.равно(x) также должно быть равно true. (И если первое возвращает значение false, то второе также должно)

  • Транзитивный : Как расширение симметричного свойства. Если x.равно(y) и y.равно(z) тогда x.equals(z) должно быть равно true.

  • Последовательный : Многократные вызовы x.equals(y) должны последовательно возвращать один и тот же результат. Таким образом, не должно быть никаких побочных эффектов от выполнения.

  • Последний элемент, в меньшей степени свойство, заключается в том, что для любого ненулевого x . x.equals(null) должно быть равно false.

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

Рефлексивный – Это первое свойство довольно простое. Объекты должны быть равны самим себе. Обычно это довольно легко сделать, особенно учитывая, что мы не переопределяем функцию equals , мы получаем это свойство бесплатно.

Симметрия – Следующее свойство более мясистое. В этом говорится, что если x говорит, что это равно y , y также следует сказать , что он равен x . Давайте рассмотрим пример класса, который не соответствует этому свойству.

public final class CaseInsensitiveString {
  private final String value;

  public CaseInsensitiveString(String value) {
    this.value = Objects.requireNonNull(value);
  }

  @Override
  public boolean equals(Object o) {
    if (o instanceof CaseInsensitiveString) {
      return value.equalsIgnoreCase((CaseInsensitiveString)o);
    } else if (o instanceof String) {
      // This breaks symmetry.
      return value.equalsIgnoreCase(o);
    }
    return false;
  }
}

В приведенном выше классе мы видим, что если String передается в объект String без учета регистра, чтобы проверить равенство, мы проверяем строку без учета регистра. Однако, если строка сравнивалась со строкой без регистра, она учитывала бы регистр строки. Таким образом, поскольку строка без учета регистра.equals(строка) не обязательно равна string.equals(CaseInsensitiveString) . Итак, как бы мы исправили вышеуказанную реализацию? Путем упрощения его:

@Override
public boolean equals(Object o) {
  return o instanceof CaseInsensitiveString &&
    ((CaseInsensitiveString)o).value.equalsIgnoreCase(o);
}

Переходный Вот тут-то все и становится по-настоящему весело. Свойство transitive говорит, что если a.равно(b) и b.равно(c) , то это должно означать, что a.равно(c) . Итак, давайте посмотрим на примере, как это свойство может быть нарушено. Рассмотрим следующий класс:

public class Animal {
  private final int numberOfLegs;

  public Animal(int numberOfLegs) {
    this.numberOfLegs = numberOfLegs;
  }

  @Override
  public boolean equals(Object o) {
    if (!(o instanceof Animal)) {
      return false;
    }

    return ((Animal)o).numberOfLegs == numberOfLegs;
  }
}

Итак, как вы можете видеть, у нас есть очень простой (и супер надуманный) пример класса для хранения животных, где, по-видимому, мы определяем, являются ли два животных одинаковыми, если у них одинаковое количество ног. Теперь давайте расширим этот класс:

public class Dog extends Animal {
  private final String breed;

  public Dog(String breed, int numberOfLegs) {
    super(numberOfLegs);
    this.breed = breed;
  }
}

Хорошо, итак, мы добавили одно значение в этот класс поверх того, что Животное уже было. Как мы должны написать функцию equals? Давайте рассмотрим одну попытку:

@Override
public boolean equals(Object o) {
  if(!(o instanceof Dog)) {
    return false;
  }

  Dog dog = (Dog) o;

  return super.equals(dog) && breed.equals(dog.breed);
}

Поэтому неудивительно, что мы утверждаем, что одна собака равна другой, если у нее одинаковое количество ног и она той же породы. Кажется достаточно простым, но видите ли вы, какую собственность мы, возможно, нарушили? Мы действительно нарушили симметрию. Давайте посмотрим, как это сделать. Рассмотрим следующие два объекта.

Animal animal = new Animal(4);
Dog pitbull = new Dog("Pitbull", 4);

Так что, если бы у нас был animal.equals(pitbull) , он вернул бы true однако, если мы перевернем его и выполним |/pitbull.equals(animal) это вернет false . Упс. Возможно, если бы мы сделали функцию equals немного умнее, мы смогли бы это исправить.

@Override
public boolean equals(Object o) {
  if(!(o instanceof Animal)) {
    return false;
  }

  if(!(o instanceof Dog)) {
    return o.equals(this)
  }

  Dog dog = (Dog) o;

  return super.equals(dog) && breed.equals(dog.breed);
}

Отлично! Мы исправили нашу проблему с симметрией, но как мы поступили с нашим транзитивным свойством?

Dog pitbull = new Dog("Pitbull", 4);
Animal animal = new Animal(4);
Dog basset = new Dog("Basset", 4);

В этом случае pitbull.equals(animal) будет равно true и animal.equals(бассет) равно true, но pitbull.equals(бассет) не равно true . О-о-о. Так что же делать? Как нам это исправить? Что ж, оказывается эта проблема на самом деле не поддается исправлению . Верно. То, что мы только что увидели здесь, является фундаментальной проблемой, связанной с этими отношениями эквивалентности в объектно-ориентированных языках. Один из предлагаемых способов устранения этой проблемы, который иногда предлагается, – это просто использовать getClass() вместо проверок instanceof . Это приводит к тому, что эквивалентность допускается только в том случае, если реализующие классы имеют один и тот же тип. Это, однако, нарушает принцип подстановки Лискова и нарушает некоторые концепции объектно-ориентированного проектирования. Принцип подстановки Лискова просто гласит, что объект типа подтипа должен иметь возможность заменять любое существование одного из его родительских типов. Метод, который Эффективный Java pitches заключается в том, чтобы отдавать предпочтение композиции, а не наследованию, что является советом позже в книге. Высокоуровневый обзор этого метода заключается в том, что вместо наследования типа вы просто храните экземпляр этого типа, что позволяет лучше контролировать, как можно использовать различные фрагменты данных, и тогда у нас не возникнет проблем с принципом подстановки Лискова.

Последовательность : Это приводит нас к последовательности. Это в основном сводится к тому, чтобы не полагаться на ненадежные ресурсы. Так что не используйте генераторы случайных чисел как часть вашей проверки на равенство.

Ненулевое значение : Это последнее довольно просто. Если кто-то передает null в функцию equals , просто верните false и не вызывайте NullPointerException .

Наконец, давайте рассмотрим некоторые шаги для высококачественной реализации equals в соответствии с эффективной Java.

  • Используйте оператор == , чтобы проверить, являются ли объекты одной и той же ссылкой. Это хорошая оптимизация производительности.
  • Используйте оператор instanceof , чтобы убедиться, что вам был предоставлен объект правильного типа, а также для обработки требования ненулевости .
  • Приведите ваш объект к правильному типу.
  • Для каждого “значимого” поля класса проверьте равенство. Для примитива вне float и Двойной (который вы должны использовать Поплавок.сравните и Double.compare() соответственно) проверьте равенство с помощью == . Для ссылочных типов используйте рекурсивные вызовы equals() . Чтобы избежать NullPointerExceptions, рассмотрите возможность использования Objects.equals для выполнения этих сравнений. Другие вещи, о которых стоит подумать, – это то, можете ли вы сравнить более дешевые поля перед более дорогими полями.
  • Всегда переопределять Хэш-код если вы переопределяете равно .
  • Не пытайся быть слишком умным
  • Убедитесь, что вы правильно выполняете переопределение. Это общедоступное логическое значение равно(объект o) не общедоступные логические значения(MyType o) . Это одна из причин, по которой полезно использовать аннотацию @Override .

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

Итак, есть ли простая кнопка? Вроде как так и есть. Как уже говорилось ранее, Lombok – отличный инструмент для разработки Java и избавления от шаблонных шаблонов. У Ломбока есть отличная аннотация @EqualsAndHashCode . Я бы настоятельно рекомендовал использовать его, а не писать самостоятельно. IDE также часто имеют встроенные инструменты, которые помогают генерировать эти методы. Вся эта автогенерация великолепна но я все еще думаю, что важно знать, что делает хорошую функцию equals, плюс это поможет вам стать лучшим разработчиком и более полно понять, как создается магия.

Итак, каков ваш опыт работы с методом equals ? Какие-нибудь страшные истории? Какие-нибудь странные ошибки? Дайте нам знать в комментариях.

Оригинал: “https://dev.to/kylec32/effective-java-tuesday-obey-the-equals-contract-4df4”