Один из популярных вопросов для джуниров: рассказать про equals и hashCode. У меня этот вопрос встречался на двух собеседованиях из семи. Впрочем, будь у меня изначально уровень чуть пониже, то этот вопрос встречался бы чаще. Если нет опыта, то его наверняка зададут.
С примитивами всё просто. Мы сравниваем через == и всё хорошо. Но почему нельзя аналогично делать с объектами? Потому что == сравнивает по ссылкам, а не по параметрам объекта. Грубо говоря, == сравнивает, что два кота находят в одной квартире. А equals сравнивает именно параметры котов (цвет, возраст, длину хвоста). Чтобы сравнивать именно по параметрам надо переопределять метод equals. Кстати, задумывались ли вы о том, как этот метод реализован в String? Известно, что строки рекомендуется сравнивать именно так, но что там под капотом? Там обычное посимвольное сравнение.
Предположим, у нас есть три кота:
- Васька. Рыжий. 8 лет. Живёт в Москве.
- Мурзик. Белый. 3 года. Тоже живёт в Москве.
- Киса. Черная. 1 год. Живёт в Перми.
Если мы попробуем сейчас сравнить Васька.equals(Мурзик), то будет исключение. Просто потому что джава не знает, как именно сравнивать котов. Достаточно проверить только имена или надо проверять каждый параметр? Или можно посмотреть только города? Как точно надо сравнивать?
Правила для переопределения equals. Они не просто из головы взяты, а из документации: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/Object.html:
- Рефлексивность. Объект всегда должен быть равен самому себе. Т.е. кот Мурзик всегда должен быть равен себе. В коде: if (this == murzik) return true;
- Симметричность. Если Васька равен Мурзику, то и Мурзик равен Ваське.
- Транзитивность. Если Васька равен Мурзику и Васька равен Кисе, то Мурзик тоже равен Кисе.
- Постоянность. Если параметры котов не менялись (возраст не увеличивался, хвост не отрастал), то сколько бы мы не вызывали метод equals, то он всегда должен возвращать один результат.
- При сравнение кота с null всегда должно возвращаться false.
Пример переопределения: сравниваем, что это не одинаковые объекты; проверяем на null и что у нас передан точно кот, а не собака; сравниваем параметры (цвет, возраст, город).
HashCode → это обычное значение int, которое соответствует объекту. Мы самостоятельно решаем как его вычислять. Например, вот как он устроен в String:
Есть несколько простых правил:
- Для одного объекта хэшкод всегда одинаковый и не меняется. Т.е. мы не можем переопределить метод и генерировать там рандомное число;
- При генерации хэшкода надо использовать все параметры, а не какой-то один. Т.е. в классе Cat не стоит писать, что hashCode = 31, а то получится, что у всех котов он одинаковый и весь смысл теряется.
- У двух разных объектов может быть одинаковый hashCode. Почему так происходит? Дело в том, что hashCode ограничен интовым значением. Там есть диапазон, поэтому результат может повторяться.
- У двух одинаковых объектов всегда одинаковые хэшкоды.
- Если хэшкоды разные, то объекты всегда разные.
Почему у одинаковых объектов не могут быть разные хэшкоды? Предположим есть два одинаковых кота: Мурзик и Васька. Мурзика мы положили в одну коробку, а Ваську в другую. В случае HashMap, когда мы захотим найти Ваську, то будем искать его в первой коробке. Мы же знаем, что там должен быть нужный нам кот с черными лапами и белыми усами. Но его там не будет. Точнее будет Мурзик, а не Васька. В итоге поиск не сработает. Поэтому если у нас два одинаковых кота, то они обязательно должны быть в одной коробке.
А зачем вообще нужен этот hashCode? Он использует в различных коллекциях. Выше как раз пример. А ещё при сравнении объектов сначала удобно проверить на равенство хэшкодов и только потом сравнивать через equals. Так быстрее по скорости, потому что equals → немного медленно работает. Гораздо быстрее сравнивать два числа и просто не проверять дальше, если они не равны.
И общие правила для сравнения объектов:
- Если есть два объекта и они равны по equals, то и hashCode их равны. Если не равны, то неправильно реализовали.
- Если два объекта не равны по equals, то hashCode могут быть равны, а могут и нет. Потому что Int ограничение. Смотрите выше.