equals()
и hashCode()
. Мы встречаемся с ними не впервые: в начале курса JavaRush была небольшая лекция об equals()
— прочитай ее, если подзабыл или не встречал ранее.

==
” — плохая идея, потому что “==
” сравнивает ссылки.
Вот наш пример с машинами из недавней лекции:
public class Car {
String model;
int maxSpeed;
public static void main(String[] args) {
Car car1 = new Car();
car1.model = "Ferrari";
car1.maxSpeed = 300;
Car car2 = new Car();
car2.model = "Ferrari";
car2.maxSpeed = 300;
System.out.println(car1 == car2);
}
}
Вывод в консоль:
false
Казалось бы, мы создали два идентичных объекта класса Car
: все поля у двух машин одинаковые, но результат сравнения все равно false.
Причина нам уже известна: ссылки car1
и car2
указывают на разные адреса в памяти, поэтому они не равны.
Мы же все-таки хотим сравнить два объекта, а не две ссылки. Лучшее решение для сравнения объектов — метод equals()
.
Метод equals()
Возможно, ты помнишь, что этот метод мы не создаем с нуля, а переопределяем — ведь методequals()
определен в классе Object
.
Однако в обычном виде толку от него мало:
public boolean equals(Object obj) {
return (this == obj);
}
Вот так метод equals()
определен в классе Object
. То же самое сравнение ссылок. Зачем его сделали таким?
Ну а откуда создателям языка знать, какие объекты в твоей программе считать равными, а какие — нет? :)
В этом заключается основная идея метода equals()
— создатель класса сам определяет характеристики, по которым проверяется равенство объектов этого класса. Сделав это, ты переопределяешь метод equals()
в своем классе.
Если тебе не совсем понятен смысл «сам определяешь характеристики», давай рассмотрим пример.
Вот простой класс человека — Man
.
public class Man {
private String noseSize;
private String eyesColor;
private String haircut;
private boolean scars;
private int dnaCode;
public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
this.noseSize = noseSize;
this.eyesColor = eyesColor;
this.haircut = haircut;
this.scars = scars;
this.dnaCode = dnaCode;
}
//геттеры, сеттеры и т.д.
}
Допустим, мы пишем программу, которая должна определять, являются ли два человека родственниками-близнецами, или это просто двойники.
У нас есть пять характеристик: размер носа, цвет глаз, прическа, наличие шрамов и результаты биологического теста ДНК (для простоты — в виде кодового числа).
Как ты думаешь, какие из этих характеристик позволят нашей программе определить родственников-близнецов?

equals()
?
Нам нужно его переопределить в классе Man
с учетом требований нашей программы.
Метод должен сравнивать поле int dnaCode
двух объектов, и если они равны, значит, и объекты равны.
@Override
public boolean equals(Object o) {
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Неужели так просто? Не совсем. Мы кое-что упустили.
В данном случае для наших объектов мы определили всего одно «значимое» поле, по которому устанавливается их равенство — dnaCode
.
А теперь представь, что таких «значимых» полей у нас было бы не 1, а 50. И если все 50 полей у двух объектов равны, то и объекты равны.
Такое тоже может быть. Главная проблема в том, что вычисление равенства 50 полей — затратный по времени и ресурсам процесс.
Теперь представь, что помимо класса Man
у нас есть класс Woman
с точно такими же полями, как и в Man
.
И если твоими классами будет пользоваться другой программист, он запросто может написать в своей программе что-то типа:
public static void main(String[] args) {
Man man = new Man(........); //куча параметров в конструкторе
Woman woman = new Woman(.........);//такая же куча параметров.
System.out.println(man.equals(woman));
}
Проверять значения полей в данном случае бессмысленно: мы же видим, что перед нами объекты двух разных классов, и они не могут быть равны в принципе!
Значит в метод equals()
нам нужно поместить проверку — сравнение объектов двух одинаковых классов. Хорошо, что мы об этом подумали!
@Override
public boolean equals(Object o) {
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Но, может мы забыли что-то еще?
Хм… Как минимум, надо бы проверить, что мы не сравниваем объект сам с собой! Если ссылки А и Б указывают на один адрес в памяти, значит, это один и тот же объект, и нам тоже не надо тратить время и сравнивать 50 полей.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Кроме того, не помешает добавить проверку на null
: никакой объект не может быть равен null
, и в таком случае нет смысла в дополнительных проверках.
С учетом всего этого, наш метод equals()
для класса Man
будет выглядеть так:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Man man = (Man) o;
return dnaCode == man.dnaCode;
}
Мы проводим все первоначальные проверки, о которых сказали выше. Если в итоге оказалось, что:
- мы сравниваем два объекта одного класса
- это не один и тот же объект
- мы сравниваем наш объект не c
null
dnaCode
двух объектов.
Переопределяя метод equals()
, обязательно соблюдай эти требования:
Рефлексивность.
Любой объект должен быть
equals()
самому себе.
Мы уже учли это требование. В нашем методе указано:if (this == o) return true;
Симметричность.
Если
a.equals(b) == true
, то иb.equals(a)
должно возвращатьtrue
.
Этому требованию наш метод тоже соответствует.Транзитивность.
Если два объекта равны какому-то третьему объекту, значит, они должны быть равны друг и другу.
Еслиa.equals(b) == true
иa.equals(c) == true
, значит проверкаb.equals(c)
тоже должна возвращать true.Постоянность.
Результаты работы
equals()
должны меняться только при изменении входящих в него полей. Если данные двух объектов не менялись, результаты проверки наequals()
должны быть всегда одинаковыми.Неравенство с
null
.Для любого объекта проверка
a.equals(null)
должна возвращать false
Это не просто набор каких-то «полезных рекомендаций», а именно жесткий контракт методов, прописанный в документации Oracle
Метод hashCode()
Теперь поговорим о методеhashCode()
. Зачем он нужен?
Ровно для той же цели — сравнения объектов. Но ведь у нас уже есть equals()
! Зачем же еще один метод?
Ответ прост: для повышения производительности.
Хэш-функция, которая представлена в Java методом hashCode()
, возвращает числовое значение фиксированной длины для любого объекта. В случае с Java метод hashCode()
возвращает для любого объекта 32-битное число типа int
.
Сравнить два числа между собой — гораздо быстрее, чем сравнить два объекта методом equals()
, особенно если в нем используется много полей.
Если в нашей программе будут сравниваться объекты, гораздо проще сделать это по хэш-коду, и только если они равны по hashCode()
— переходить к сравнению по equals()
.
Таким образом, кстати, работают основанные на хеше структуры данных — например, известная тебе HashMap
!
Метод hashCode()
, так же как и equals()
, переопределяется самим разработчиком.
И так же, как для equals()
, для метода hashCode()
есть официальные требования, прописанные в документации Oracle:
Если два объекта равны (т.е. метод
equals()
возвращает true), у них должен быть одинаковый хэш-код.Иначе наши методы будут лишены смысла. Проверка по
hashCode()
, как мы и сказали, должна идти первой для повышения быстродействия. Если хэш-коды будут разными, проверка вернет false, хотя объекты на самом деле равны (согласно нашему определению в методеequals()
).Если метод
hashCode()
вызывается несколько раз на одном и том же объекте, каждый раз он должен возвращать одно и то же число.Правило 1 не работает в обратную сторону. Одинаковый хэш-код может быть у двух разных объектов.
hashCode()
возвращает int
.
int
— это 32-битное число.
У него есть ограниченное число значений — от -2,147,483,648 до +2,147,483,647. Иными словами, всего существует чуть больше 4 миллиардов вариантов числа int
.
Теперь представь, что ты создаешь программу для хранения данных обо всех живущих людях на Земле. Каждому человеку будет соответствовать свой объект класса Man
.
На земле живет ~7.5 миллиарда человек.
Иными словами, какой бы хороший алгоритм преобразования объектов Man
в число мы ни написали, нам просто не хватит чисел. У нас всего 4,5 миллиарда вариантов, а людей намного больше. Значит, как бы мы ни старались, для каких-то разных людей хэш-коды будут одинаковыми.
Такая ситуация (совпадение хэш-кодов у двух разных объектов) называется коллизией.
Одна из задач программиста при переопределении метода hashCode()
— сократить потенциальное число коллизий насколько это возможно.
Как же будет выглядеть наш метод hashCode()
для класса Man
с учетом всех этих правил?
Вот так:
@Override
public int hashCode() {
return dnaCode;
}
Удивлен? :)
Неожиданно, но если ты посмотришь на требования, увидишь, что мы соблюдаем все.
Объекты, для которых наш equals()
возвращает true, будут равны и по hashCode()
. Если два наших объекта Man
будут равны по equals
(то есть у них одинаковый dnaCode
), наш метод вернет одинаковое число.
Рассмотрим пример посложнее.
Допустим, наша программа должна отбирать элитные автомобили для клиентов-коллекционеров.
Коллекционирование — штука сложная, и в ней много особенностей. Автомобиль 1963 года выпуска может стоить в 100 раз дороже, чем такой же автомобиль 1964 года. Красный автомобиль 1970 года может стоить в 100 раз дороже, чем синий автомобиль той же марки того же года.

Man
, мы отбросили большинство полей (т.е. характеристик человека) как незначительные и для сравнения использовали только поле dnaCode
.
Здесь же мы работаем с очень своеобразной сферой, и незначительных деталей быть не может!
Вот наш класс LuxuryAuto
:
public class LuxuryAuto {
private String model;
private int manufactureYear;
private int dollarPrice;
public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
this.model = model;
this.manufactureYear = manufactureYear;
this.dollarPrice = dollarPrice;
}
//...геттеры, сеттеры и т.д.
}
Здесь при сравнении мы должны учитывать все поля. Любая ошибка может стоить сотен тысяч долларов для клиента, поэтому лучше перестраховаться:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
if (dollarPrice != that.dollarPrice) return false;
return model.equals(that.model);
}
В нашем методе equals()
мы не забыли о всех проверках, о которых говорили ранее. Но теперь мы сравниваем каждое из трех полей наших объектов. В этой программе равенство должно быть абсолютным, по каждому полю.
А что же с hashCode
?
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = result + manufactureYear;
result = result + dollarPrice;
return result;
}
Поле model
в нашем классе — строка. Это удобно: в классе String
метод hashCode()
уже переопределен.
Мы вычисляем хэш-код поля model
, а к нему прибавляем сумму двух остальных числовых полей.
В Java есть одна небольшая хитрость, которая используется для сокращения числа коллизий: при вычислении хэш-кода умножать промежуточный результат на нечетное простое число. Чаще всего используется число 29 или 31.
Мы не будем сейчас углубляться в математические тонкости, но на будущее запомни, что умножение промежуточных результатов на достаточно большое нечетное число помогает «размазать» результаты хэш-функции и получить в итоге меньшее число объектов с одинаковым хэшкодом.
Для нашего метода hashCode()
в LuxuryAuto это будет выглядеть вот так:
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
Подробнее обо всех тонкостях этого механизма можно прочитать в этом посте на StackOverflow, а также у Джошуа Блоха в книге «Effective Java».
Напоследок еще один важный момент, о котором стоит сказать.
Каждый раз при переопределении equals()
и hashCode()
мы выбирали определенные поля объекта, которые в этих методах учитывались.
Но можем ли мы учитывать разные поля в equals()
и hashCode()
?
Технически, можем. Но это плохая идея, и вот почему:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LuxuryAuto that = (LuxuryAuto) o;
if (manufactureYear != that.manufactureYear) return false;
return dollarPrice == that.dollarPrice;
}
@Override
public int hashCode() {
int result = model == null ? 0 : model.hashCode();
result = 31 * result + manufactureYear;
result = 31 * result + dollarPrice;
return result;
}
Вот наши методы equals()
и hashCode()
для класса LuxuryAuto.
Метод hashCode()
остался без изменений, а из метода equals()
мы убрали поле model
. Теперь модель — не характеристика для сравнения двух объектов по equals()
. Но при расчете хэш-кода она по-прежнему учитывается.
Что же мы получим в результате? Давай создадим два автомобиля и проверим!
public class Main {
public static void main(String[] args) {
LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);
System.out.println("Эти два объекта равны друг другу?");
System.out.println(ferrariGTO.equals(ferrariSpider));
System.out.println("Какие у них хэш-коды?");
System.out.println(ferrariGTO.hashCode());
System.out.println(ferrariSpider.hashCode());
}
}
Эти два объекта равны друг другу?
true
Какие у них хэш-коды?
-1372326051
1668702472
Ошибка! Использовав разные поля для equals()
и hashCode()
мы нарушили установленный для них контракт!
У двух равных по equals()
объектов должен быть одинаковый хэш-код. Мы же получили для них разные значения.
Подобные ошибки могут привести к самым невероятным последствиям, особенно при работе с коллекциями, использующими хэш.
Поэтому при переопределении equals()
и hashCode()
правильно будет использовать одни и те же поля.
Лекция получилось немаленькой, но сегодня ты узнал много нового! :)
Самое время вернуться к решению задач!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ