Сравнение объектов в Java

В этой статье мы рассмотрим методики сравнения объектов в Java и какие инструменты нам для этого понадобятся. Как вы сможете понять из этой статьи задача сравнения объектов не такая уж, и тривиальная и выработать единый и универсальный подход для сравнения всех объектов едва ли представляется возможным. Сравнение объектов в Java выполняется с помощью оператора сравнения == и метода equals(). Если оператор сравнения можно использовать с примитивами, то метод equals() используется только с экземплярами классов. При их использовании проверяется ссылаются ли переменные на один и тот же объект в памяти. Рассмотрим пример:

Переменная ob1 содержит в себе ссылку на объект класса Object, а переменная ob2 просто ее копирует при объявлении, в итоге две переменных ссылаются на один и тот же объект в памяти, что при сравнении этих двух переменных всегда возвращается true. Другой пример:

Теперь при объявлении каждой из переменных вызывается оператор new, который для каждой из них создает свой экземпляр класса Object, поэтому при сравнении этих двух переменных каждый раз возвращается false, потому что обе переменные указывают на разные объекты в разных участках памяти. Давайте теперь сравним что-то более конкретное, например, две строки:

Как видите поведение метода equals() для класса String несколько отличается. Это происходит по одной простой причине, метод equals() определен в классе Object, поэтому все классы в Java его наследуют и вольны переопределять, что и было сделано в классе String. Как видите методом equals() проверяются на соответствие символы двух строк, если они совпадают, то метод возвращает true. Перед тем как приступить к переопределению метода equals() и комплексному сравнению объектов созданных нами классов, мы рассмотрим инструменты, которые нам для этого понадобятся: оператор instanceof и метод getClass().



Оператор instanceof.

Оператор instanceof служит для проверки к какому классу принадлежит объект. a instanceof B возвращает истину, если в переменной a содержится ссылка на экземпляр класса B, подкласс B (напрямую или косвенно, иначе говоря, состоит в иерархии наследования классов) или реализует интерфейс B (так же напрямую или косвенно). Давайте подробнее рассмотрим, как это работает на примере:

Иерархия классов очень проста, главный класс A, классы B и C от него наследуются. Посмотрим как поведет себя оператор instanceof с нашими классами:

Первая проверка возвращает true, потому что aB является экземпляром класса B. Вторая проверка возвращает true, потому что aB является подклассом A. И последний вызов оператора instanceof возвращает false, несмотря на то, что оба класса B и С наследуются от A, это совершенно разные классы. На самом деле, ситуация с последним вызовом оператора instanceof сложнее, чем может показаться. Типом переменной aB является класс A, иначе говоря, она может содержать в себе любой из трех классов, в том числе класс C и компилятор это проверяет, если возникнет ситуация, при которой оператор instanceof не сможет ни при каких условиях вернуть true – возникнет ошибка компиляции:

Переменная с типом класса B на данном этапе нашей иерархии классов ни при каких условиях не может содержать в себе ссылку на класс С, поэтому возникает ошибка компиляции. Опять же, есть один нюанс, это правило не работает для интерфейсов:

Как мы видим связи между классом A и интерфейсом I нет никакой, и тем не менее ошибки компиляции не возникает, а оператор instanceof возвращает false. Всегда существует вероятность в будущем реализации интерфейса тем или иным классом, поэтому в данном случае компилятор идет нам уступки. Если же класс реализует интерфейс, то оператор instanceof вернут true:

Как упоминалось ранее все классы наследуются от класса Object либо напрямую, либо через родительский класс, поэтому a instanceof Object всегда будет возвращать true за исключение одной ситуации:

Так вот, a instanceof Object всегда будет возвращать true, только если a не равна null.

Метод getClass().

Метод getClass() возвращает класс объекта, содержащий сведения об объекте: public final Class<?> getClass(). Как Вы можете заметить метод является конечным и переопределению не подлежит. С методом getClass() все обстоит несколько проще и очевидней нежели с оператором instanceof.

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

Метод equals().

Почему мы так подробно рассматривали оператор instanceof? Потому что это будет наша первая проверка, хотя многие предпочитают использовать метод getClass(), но обо всем по порядку. Как говорилось выше, метод equals() содержится в классе Object и наследуется всеми классами, в которых мы вправе его переопределить, давайте это сделаем:

В нашем примере метод equals() всегда будет возвращать true, что и было продемонстрировано. Не смотря на то, что в памяти было создано два объекта Eq, метод equals() все равно вернул истину. Единственное на что хотелось бы обратить внимание, это аннотация @Override. Она используется для явного обозначения переопределения метода, если бы мы допустили ошибку и просто перегрузили метод, то с этой аннотацией у нас бы возникла ошибка компиляции:

В примере выше метод equals() перегружен (в качестве параметра методу передается класс Eq вместо Object), что и приводит к ошибке компиляции. Настало время рассмотреть более сложный пример:

В классе Car происходит более комплексная проверка на равенство объектов: первоначально оператор instanceof проверяет, принадлежат ли классы одной иерархии наследования, если нет, то объекты не равны, если принадлежат, то проверка идет по строковому полю name, при совпадении этого поля (ну или при совпадении названия машин) объекты будут равны. Рассмотрим пример сравнения объектов с помощью метода getClass():
Так как метод equals() является одним из ключевых в Java, существует ряд правил касающихся его работы:

  • Рефлексивность. При вызове x.equals(x) по любой ненулевой ссылке x должно возвращаться логическое значение true.
  • Симметричность. При вызове x.equals(y) по любым ссылкам x и y должно возвращаться логическое значение true, тогда и только тогда, когда при вызове y.equals(x) возвращается логическое значение true.
  • Транзитивность. Если при вызовах x.equals(y) и y.equals(z) по любым ссылкам x, y и z возвращается логическое значение true, то и при вызове x.equls(z) возвращается логическое значение true.
  • Согласованность. Если объекты, на которые делаются ссылки x и y, не изменяются, то при повторном вызове x.equals(y) должно возвращаться тоже самое значение.
    При вызове x.equals(null) по любой непустой ссылке x, должно возвращаться логическое значение false.



Метод hashCode()

Хэш-код — это целое число, генерируемое на основе конкретного объекта. Метод hashCode() тесно взаимосвязан с методом equals() (если x и y – разные объекты, то с высокой долей вероятности должны различаться результаты вызовов hashCode() для этих объектов), поэтому Oracle рекомендует при переопределении метода equals() так же переопределять метод hashCode():

Теперь класс Car помимо метода equals() еще переопределяет метод hashCode(), который возвращает хэш-код строкового поля name. Пример с классом машин был сознательно упрощен, конечно, если вы занимаетесь разработкой классов, помимо переопределения рассматриваемых методов, не помешает проверка на передачу в конструктор пустых значений (null) или воспользоваться методом Objects.hashCode, который возвращает 0 если его аргумент имеет пустое значение (для Java 7 и выше). В 7ой версси Java был добавлен еще один замечательный метод Objects.hash(), который в качестве параметра получает аргумент переменной длины (varargs) и возвращает хэш-код состоящий из хэш-кодов переданных элементов: public static int hash(Object… values). Пример использования:

Вывод:
6911431

Как видите получить хэш-код для двух переменных не так сложно – всего одна строчка кода! Класс Objects содержит в себе много полезных методов и я вам настоятельно рекомендую изучить его более подробно.

В попытке достичь совершенства.

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

Приступим к реализации:

1. Всегда используйте аннотацию @Override это не только сделает Ваш код более читабельным, но и позволит избежать ошибки с перегрузкой метода equals().

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

3. Далее нужно обязательно проверить является ли ссылка compareObject пустой (null), если да то следует вернуть логическое значение false:

4. Настало время сравнить классы. Если семантика сравнения может измениться в подклассе, лучшим выбором для сравнения будет метод getClass():

В ином случае проверку необходимо производить с помощью метода instanceof:

5. Далее приведите тип объекта compareObject к типу переменной требуемого класса:

6. Определитесь какие поля класса вы будете сравнивать, для примитивных типов используйте оператор сравнения ==, для ссылочных типов метод equals():

Давайте посмотрим на общую картину:

Обратите внимание, что в примере, у одинаковых объектов хэш-код совпадает.



Вместо послесловия

Так же как и гвозди можно забивать как камнем, так и молотком, так и объекты можно сравнивать с использованием готовых инструментов. В этом нам поможет одна замечательная библиотека, которая сделает все за нас — Apache Commons Lang. Вообще, проект Apache Commons очень интересен сам по себе и я о нем вам расскажу более подробно в будущих статьях, а пока вернемся к методу equals().

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

Уже выглядит не так элегантно как хотелось бы, теперь нам надо проверять принадлежность объектов одному и тому же классу и приводить типы.

Если у Вас есть какие-либо предложения по совершенствованию переопределения метода equals(), Вы их можете направить на адрес javanerd.ru@gmail.com и я их обязательно включу в статью.

Site Footer