Методы Java hashCode() и equals()

Узнайте о методах Java hashCode() и equals(), их реализации по умолчанию и о том, как правильно их переопределить. Также мы научимся реализовывать эти методы с использованием сторонних классов HashCodeBuilder и EqualsBuilder.

Методы hashCode() и equals() определены в классе Object, который является родительским классом для всех классов Java. По этой причине все объекты Java наследуют реализацию этих методов по умолчанию.

1. Методы hashCode() и equals()

  • equals(Object otherObject) – проверяет равенство двух объектов. Его реализация по умолчанию просто проверяет ссылки на объекты двух объектов, чтобы проверить их равенство. По умолчанию два объекта равны тогда и только тогда, когда они ссылаются на одну и ту же область памяти. Большинство классов Java переопределяют этот метод, чтобы предоставить собственную логику сравнения.
  • hashcode() – возвращает уникальное целочисленное значение для объекта во время выполнения. По умолчанию значение Integer выводится из адреса памяти объекта в куче(но это не обязательно). Хэш-код объекта используется для определения местоположения индекса, когда этот объект необходимо сохранить в некоторой структуре данных, подобной HashTable.

1.1. Контракт между hashCode() и equals()

Переопределение hashCode() обычно необходимо всякий раз, когда переопределяется equals(), чтобы сохранить общий контракт для метода hashCode(), который гласит, что равные объекты должны иметь равные хеш-коды.

  • Всякий раз, когда он вызывается для одного и того же объекта более одного раза во время выполнения приложения Java, hashCode() должен последовательно возвращать одно и то же целое число, при условии, что никакая информация, используемая в сравнениях на равенство для объекта, не изменяется.
    Это целое число не обязательно должно оставаться неизменным между двумя выполнениями одного и того же приложения или программы.
  • Если два объекта равны согласно методу equals(), то вызов hashCode() для каждого из двух объектов должен давать одинаковый целочисленный результат.
  • Если два объекта не равны согласно equals(), то не обязательно, чтобы вызов hashCode() для каждого из обоих объектов давал разные целочисленные результаты.
    Однако программист должен знать, что создание различных целочисленных результатов для неравных объектов может повысить производительность хэш-таблиц.

2. Переопределение поведения по умолчанию

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

Давайте разберемся, почему нам нужно переопределять методы equals и hashcode.

2.1. Поведение по умолчанию

Давайте рассмотрим пример, где ваше приложение имеет объект Employee. Давайте создадим минимально возможную структуру класса Employee:

public class Employee{private Integer id;private String firstname;private String lastName;private String department;//Setters and Getters}

Выше класс Employee имеет некоторые фундаментальные атрибуты и их методы доступа. Теперь рассмотрим простую ситуацию, когда вам нужно сравнить два объекта Employee. Оба объекта employee имеют одинаковый идентификатор.

public class EqualsTest {public static void main(String[] args) {Employee e1 = new Employee();Employee e2 = new Employee();e1.setId(100);e2.setId(100);System.out.println(e1.equals(e2)); //false}}

Никакого приза за угадывание. Вышеуказанный метод выведет «false».

Но правильно ли это, зная, что оба объекта представляют одного и того же сотрудника? В приложении реального времени это должно возвращать true.

2.2 Переопределение только equals()

Чтобы добиться корректного поведения приложения, нам необходимо переопределить метод equals(), как показано ниже:

public boolean equals(Object o) {if(o == null){return false;}if(o == this){return true;}if(getClass() != o.getClass()){return false;}Employee e =(Employee) o;return(this.getId() == e.getId());}

Добавьте этот метод к классу Employee, и EqualsTest начнет возвращать «true».

Ну что, мы закончили? Пока нет. Давайте еще раз протестируем измененный выше класс Employee другим способом.

import java.util.HashSet;import java.util.Set;public class EqualsTest{public static void main(String[] args){Employee e1 = new Employee();Employee e2 = new Employee();e1.setId(100);e2.setId(100);//Prints 'true'System.out.println(e1.equals(e2));Set<Employee> employees = new HashSet<Employee>();employees.add(e1);employees.add(e2);System.out.println(employees); //Prints two objects}}

В приведенном выше примере во втором операторе печати выводятся два объекта.

Если оба объекта employee были равны, в Set, который хранит уникальные объекты, должен быть только один экземпляр внутри HashSet, потому что оба объекта ссылаются на одного и того же employee. Что мы упускаем??

2.3. Переопределение hashCode() также необходимо

Нам не хватает второго важного метода hashCode(). Как говорится в документации Java, если мы переопределяем equals(), то мы должны переопределить hashCode(). Так что давайте добавим еще один метод в наш класс Employee.

@Overridepublic int hashCode(){final int PRIME = 31;int result = 1;result = PRIME * result + getId();return result;}

После добавления вышеуказанного метода в класс Employee второй оператор начинает печатать только один объект во втором операторе и таким образом проверяет истинное равенство e1 и e2.

3. EqualsBuilder и HashCodeBuilder

Apache Commons предоставляет два превосходных служебных класса HashCodeBuilder и EqualsBuilder для генерации хэш-кода и методов equals.

Мы можем использовать эти классы следующим образом.

import org.apache.commons.lang3.builder.EqualsBuilder;import org.apache.commons.lang3.builder.HashCodeBuilder;public class Employee{private Integer id;private String firstname;private String lastName;private String department;//Setters and Getters@Overridepublic int hashCode(){final int PRIME = 31;return new HashCodeBuilder(getId()%2==0?getId()+1:getId(), PRIME).toHashCode();}@Overridepublic boolean equals(Object o) {if(o == null)return false;if(o == this)return true;if(o.getClass() != getClass())return false;Employee e =(Employee) o;return new EqualsBuilder().append(getId(), e.getId()).isEquals();}}

4. Генерация hashCode() и equals() в Eclipse IDE

Большинство редакторов предоставляют общие шаблоны исходного кода. Например, Eclipse IDE имеет возможность генерировать превосходную реализацию hashCode() и equals().

Щелкните правой кнопкой мыши по файлу Java -> Источник -> Сгенерировать hashCode() и equals() …

Генерация HashCode и Equals в Eclipse
Генерация hashCode() и equals() в Eclipse

5. Лучшие практики, которым стоит следовать

  1. Всегда используйте одни и те же поля для генерации hashCode() и equals(). Как в нашем случае, мы использовали идентификатор сотрудника.
  2. Метод equals() должен быть последовательным(если объекты не изменяются, он должен продолжать возвращать одно и то же значение).
  3. Всякий раз, когда a.equals(b), то a.hashCode() должен быть таким же, как b.hashCode().
  4. Если мы переопределяем один метод, то мы должны переопределить и другой метод.

6. Особое внимание при объявлении в субъекте JPA

Если вы имеете дело с ORM, обязательно всегда используйте геттеры и никогда не используйте ссылки на поля в hashCode() и equals(). Потому что в ORM иногда поля загружаются лениво и недоступны, пока мы не вызовем их геттеры.

Например, в нашем классе Employee, если мы используем e1.id == e2.id. Очень возможно, что поле id загружается лениво. Поэтому в этом случае поле id внутри методов может быть равно нулю или null, что приведет к некорректному поведению.

Но если используется e1.getId() == e2.getId(), мы можем быть уверены, что даже если поле загружается отложенно, вызов метода получения поля сначала заполнит поле.

Прокрутить вверх