Equals и hashCode
equals отвечает на вопрос о логическом равенстве объектов. hashCode
возвращает целое число, по которому хеш-коллекция выбирает бакет. Hash не
доказывает равенство: одинаковое число может быть у разных объектов.
Контракт equals
Для ненулевых ссылок отношение должно быть:
- рефлексивным:
x.equals(x); - симметричным:
x.equals(y) == y.equals(x); - транзитивным;
- согласованным, пока значимые поля не меняются;
x.equals(null)всегда возвращаетfalse.
Контракт hashCode
Если a.equals(b), то a.hashCode() == b.hashCode(). Обратное неверно.
Повторный вызов для неизменённого объекта возвращает то же число в пределах
одного запуска. В hash должны участвовать те же значимые поля, что и в equals.
final class Coordinate {
private final int x;
private final int y;
Coordinate(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object other) {
if (this == other) return true;
if (!(other instanceof Coordinate that)) return false;
return x == that.x && y == that.y;
}
@Override
public int hashCode() {
int result = Integer.hashCode(x);
return 31 * result + Integer.hashCode(y);
}
}У String hash вычисляется как полиномиальная свёртка с множителем 31, а не
алгоритмом djb2. Обычно ручная формула не нужна: подходят records,
Objects.hash или генерация IDE. В горячем коде ручной вариант создаёт меньше
временных объектов, чем Objects.hash.
Наследование и изменяемость
Равенство через getClass() не считает равными базовый объект и наследника.
Проверка через instanceof допускает наследника, но легко нарушает симметрию,
если подкласс добавляет значимые поля. Для value object проще использовать
final-класс или record.
Нельзя менять значимые поля ключа внутри HashMap или HashSet: hash и
бакет перестанут соответствовать текущему состоянию объекта.
Частые ошибки
- переопределён только один из двух методов;
- equals сравнивает больше полей, чем hashCode;
- массивы сравниваются через
Objects.equals, а неArrays.equals; - в equals используется
==для строк и других объектов; - hashCode считается из случайного числа или текущего времени.
Вопросы для собеседования
- Могут ли разные объекты иметь один hash? Да, это коллизия.
- Почему при равном hash вызывается equals? Hash лишь сужает область поиска.
- Нужно ли переопределять hashCode вместе с equals? Да, иначе ломается контракт хеш-коллекций.
- Чем
==отличается от equals? Для ссылок==сравнивает идентичность, equals — заданное классом логическое равенство. - Что генерирует record? В том числе equals и hashCode по компонентам.
- Почему изменяемый ключ опасен? После изменения его ищут уже по другому hash.