Методы equals()
и hashCode()
помогают сравнивать объекты. Без них пришлось бы использовать много if-ов, чтобы сравнить по отдельности поля каждого объекта. А благодаря equals()
и hashCode()
вы делаете код проще для чтения и понимания — никаких лишних конструкций.
Метод equals()
нужен для того, чтобы сравнивать между собой объекты.В стандартной реализации он берёт один объект и сравнивает его с текущим объектом. Если ссылки на них равны, возвращается True, если не равны — возвращается False.
В свою очередь, hashCode()
генерирует целочисленный код экземпляра класса. Если вы делаете переопределение equals()
в Java, то необходимо переопределять hashCode()
, иначе вы можете столкнуться с ошибками.
cloud
В стандартной реализации equals() выглядит так:
public boolean equals(Object obj) {
return (this == obj);
}
Посмотрим на практике, как это работает. В следующем примере представлен класс User с двумя переменными nickname и rating. Мы создаём два экземпляра, передаём в них одинаковые значения и сравниваем их:
class User {
private String nickname;
private int rating;
User(String nickname, int rating){
this.nickname = nickname;
this.rating = rating;
}
}
public class Difference {
public static void main(String[] args) {
User user1 = new User("Andrew", 250);
User user2 = new User("Andrew", 250);
//Сравниваем два объекта и выводим результат
boolean bool = user1.equals(user2);
System.out.println(bool);
}
}
По умолчанию метод equals()
возвращает True, только если ссылки двух объектов равны. Поэтому программа из примера выше вернёт False — фактически ссылки у них разные.
Проблема в том, что при такой реализации мы не решаем реальную задачу. Допустим, бизнес-логика приложения требует, чтобы выполнялась проверка состояния объектов. Визуально понятно, что поля совпадают. Это один и тот же пользователь с одним и тем же рейтингом. Однако стандартное поведение приводит к тому, что результат получается противоположным.
Исправить этот недостаток помогает переопределение метода equals()
в Java. Суть этого механизма в изменении поведения метода equals()
родительского класса в дочернем классе. Проще разобраться на примере.
Переопределим equals()
и напишем собственную логику сравнения состояний:
class Complex {
private double number1, number2;
public Complex(double number1, double number2) {
this.number1 = number1;
this.number2 = number2;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != this.getClass()) {
return faslse;
}
Complex c = (Complex) obj;
return Double.compare(number1, c.number1) == 0
&& Double.compare(number2, c.number2) == 0;
}
}
public class Main {
public static void main(String[] args) {
Complex example1 = new Complex(20, 15);
Complex example2 = new Complex(20, 15);
if (example1.equals(example2)) {
System.out.println("Equal ");
} else {
System.out.println("Not Equal ");
}
}
}
Output:
Equal
Аннотация @Override
говорит компилятору, что нужно переопределить метод в процессе компиляции. Стоит учесть, что без аннотации переопределение метода также будет работать, если компилятор найдет в родительском классе метод с такой же сигнатурой. Однако наличие аннотации полезно для контроля этого действия и читаемости кода. Если же мы повесим аннотацию над методом, которого нет в родительском классе, то получим ошибку при сборке приложения.
Сам метод теперь состоит из трёх частей. Рассмотрим их подробнее.
Если объект сравнивается с самим собой, должно вернуться True:
if (obj == this) {
return true;
}
Смотрим, относится ли объект к классу Complex. Возвращаем False, если это не так:
if (obj == null || obj.getClass() != this.getClass()) {
return faslse;
}
Приводим тип экземпляра к Complex, сравниваем элементы и возвращаем соответствие:
Complex c = (Complex) obj;
return Double.compare(number1, c.number1) == 0
&& Double.compare(number2, c.number2) == 0;
}
При изменении работы метода нужно придерживаться правил переопределения equals() в Java:
Существуют также некоторые ограничения на переопределение equals(). Например, переопределять метод нет смысла, если каждый объект уникален. Кроме того, это относится к классам, которые предназначены не для работы с данными, а для предоставления определённого поведения.
Ещё одна ситуация, когда метод не переопределяют, — использование класса, экземпляры которого сравнивать бессмысленно. Наглядный пример — java.util.Random. Суть этого класса в том, чтобы возвращать случайные последовательности чисел. Экземпляры этого класса не должны быть равными, иначе в них нет смысла.
Когда вы меняете логику работы equals(), настоятельно рекомендуется также переопределять логику работы hashCode(). Если вы не сделаете это, у одинаковых объектов могут оказаться разные хэш-коды. По этой причине, например, коллекции на основе хэшей не будут работать так, как от них ожидают.
Благодаря тому, что hashCode() генерирует уникальный идентификатор, сравнивать состояния объектов становится проще. Если идентификаторы отличаются, equals() можно вообще не запускать. Если идентификаторы одинаковые, нужно выполнить equals() и проверить свойства объектов.
Плохой пример переопределения hashCode() — возврат константы. Например, вот так:
@Override
public int hashCode() {
return 35;
}
На практике это создаёт огромные проблемы. Хэш-значение не будет меняться при изменении состояния. Допустим, вы измените значения полей. Хэш-код останется прежним.
В определении хэш-значения должны принимать участие только те поля, которые используются в equals(). Кроме того, нужна база — основу для вычисления хэша. Обычно базой делают число 31, но вы можете установить любое другое значение.
Правила вычисления:
После обработки каждого поля вы должны прибавлять полученный результат к базе и предыдущим результатам. После прохождения по всем полям верните итоговый хэш-код.
Допустим, вы хотите переопределить hashCode() для класса Person:
public class Person {
private int age;
private int number;
private double salary;
private String name;
private CarKey carKey;
public Person(int age, int number, String name, double salary, CarKey carKey) {
this.age = age;
this.number = number;
this.name = name;
this.salary = salary;
this.carKey = carKey;
}
@Override
public int hashCode() {
int result = 31;
result = result * 17 + age;
result = result * 17 + number;
long lnum = Double.doubleToLongBits(salary);
result = result * 17 + (int)(lnum ^ (lnum >>> 32));
result = result * 17 + name.hashCode();
result = result * 17 + carKey.hashCode();
return result;
}
// Здесь уже можно переопределить equals()
// …
}
Начиная с Java 7 доступы вспомогательные методы для создания собственной реализации hashCode(). Например, для того же класса Person достаточно выполнить:
@Override
public int hashCode() {
return Objects.hash(age, number, salary, name, carKey);
}
Строгие правила переопределения разработаны и для hashCode():
Придерживайтесь этих правил при написании своих версий equals() и hashCode(). Помните, что методы нужно переопределять вместе, иначе вы можете столкнуться с тем, что экземпляры с одинаковым состоянием будут определены как разные.
Подготовили для вас выгодные тарифы на облачные серверы
Хорошее уточнение, спасибо! Обновили код в статье ✍️