Статьи по Java

Метод Equals в Java. Как правильно его реализовать?

В языке Java все классы наследуются от класса “java.lang.Object”. Среди наследуемых методов есть метод “equals”. Правильная реализация этого метода имеет решающее значение для корректности программы и – вопреки видимости – не обязательно тривиальна. Многие структуры данных зависят от их правильной реализации. Следовательно, неправильная его реализация приводит к их некорректному поведению.

Метод java.lang.Object.equals

Метод equals позволяет определить, равны ли два объекта.

Его определение по умолчанию, предоставляемое классом Object, основано на объектных ссылках.

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

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

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

Правильность реализации титульного метода можно рассматривать двумя способами:

  • Объекты должны быть равны, если они были бы равны в моделируемом мире. Например, две книги могут считаться одинаковыми, если они имеют одинаковый номер ISBN.
  • Метод equals должен удовлетворять так называемым контрактам, которые требуются стандартом Java и соблюдение которых необходимо для правильного поведения некоторых структур данных.

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

class Book { String isbn; } class Ebook extends Book { String format; }
Code language: JavaScript (javascript)

Контракты против equals

Стандарт языка требует от равноправных реализаций поддерживать следующие инварианты:

  • маневренность, то есть объект равен самому себе. Другими словами, для каждого объекта o верно, что o.equals(o) == true
  • симметрия, т.е. если первый объект равен второму, то второй также равен первому. Это означает, что если o1.equals(o2) возвращает true (false), то o2.equals(o1) также должно возвращать true (false),
  • согласованность, то есть для любых двух объектов метод o1.equals(o2) должен всегда возвращать одно и то же значение, если в объектах не произошло никаких изменений,
  • transitive – это условие, которое гарантирует, что результат операции equals является транзитивным, т.е. если у нас есть три объекта o1, o2, o3, и если o1 равен o2, а o2 равен o3, то o1 равен o3,
  • Сравнение объекта с нулевым значением всегда возвращает false.

Правильная реализация метода equals

Давайте теперь сосредоточимся на правильной реализации метода equals, то есть на той, которая сохраняет все ограничения, введенные стандартом языка.

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

Однако ситуация не так проста, поскольку equals принимает в качестве аргумента параметр типа Object:

public boolean equals(Object o)
Code language: JavaScript (javascript)

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

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

class Book { public boolean equals(Object o){ if(!(o instanceof Book)) { return false; } return this.isbn == ((Book)o).isbn; } }
Code language: JavaScript (javascript)

Эта реализация признает, что две книги (и их производные, электронные книги) равны, если их номера ISBN одинаковы. Разумная реализация для класса Ebook может выглядеть следующим образом:

class Ebook { public boolean equals(Object o) { if ((o instanceof Ebook)) { return format.equals(((Ebook) o).format) && super.equals(o); } else if ((o instanceof Book)) { return super.equals(o); } return false; } }
Code language: JavaScript (javascript)

Реализация Ebook.equals рассматривает два случая:

  • Сравниваемый объект имеет тип Ebook
  • Мы сравниваем экземпляр Ebook с экземпляром Book. Для этого мы вызываем метод из суперкласса, чтобы сравнить ту часть, которую можно сравнить – только код ISBN.

Нетрудно заметить, что оба метода обеспечивают маневренность, симметрию и согласованность. Однако давайте посмотрим, как выглядит ситуация с переходным. Равенство, реализованное таким образом, не является транзитивным. Чтобы убедиться в этом, просто проанализируйте следующий случай:

Book b1 = new Book("1"); Ebook e1 = new Ebook("1", "mobi"), e2 = new Ebook("1", "epub"); e1.equals(b1) -> true (1) b1.equals(e2) -> true (2) e1.equals(e2) -> false (3)
Code language: JavaScript (javascript)

Как мы видим, операция (3) возвращает false, что противоречит ожиданиям от переходного оператора.

А как насчет этой проходимости?

Заметим, что из представленного примера мы можем сделать немедленный, более общий вывод. А именно, что невозможно реализовать метод равенства, т.е. относиться к электронной книге так, как если бы это была обычная книга. Это – позвольте мне сказать это еще раз – естественное следствие сравнения сущностей разных типов, как в реальном мире, так и в объектно-ориентированном смысле.

Как же можно решить эту проблему? В этом случае можно сделать две вещи:

  • Запретить наследование классов, которые являются так называемыми value classes (классы, представляющие ценности или объекты реального мира). На самом деле это вполне разумно как с точки зрения моделирования реального мира, так и с технической точки зрения – такая процедура значительно упрощает реализацию equals.
  • Если по каким-то причинам наш класс должен быть открыт для возможного наследования, можно считать, что объекты разных типов никогда не бывают одинаковыми. Тогда реализация также очень проста. Шаблон метода при таком подходе может выглядеть следующим образом:
public boolean equals(Object o) { if(o == null) return false; if(o.getClass() != this.getClass()) return false; ... // просто сравните поля }
Code language: JavaScript (javascript)

При таком подходе обратите внимание, что такой метод не ведет себя корректно для производных классов.

Давайте вернемся на мгновение к основной проблеме.

Неужели невозможно реализовать метод equals так, чтобы он отвечал всем требованиям и в то же время мог сравнивать объекты с разных уровней иерархии?

В целом, существуют приемы, позволяющие правильно реализовать их.

Однако вопрос остается открытым: действительно ли разумно, что объекты разных типов могут быть равны?

Во-вторых, такие решения обычно сложны и гораздо труднее реализуемы, чем можно было бы ожидать от равных.

Поэтому кажется наиболее логичным считать, что классы, несущие ценности, должны быть классами, закрытыми для расширения.

Резюме

Реализация метода equals кажется очень простой.

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *