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 считается из случайного числа или текущего времени.

Вопросы для собеседования

  1. Могут ли разные объекты иметь один hash? Да, это коллизия.
  2. Почему при равном hash вызывается equals? Hash лишь сужает область поиска.
  3. Нужно ли переопределять hashCode вместе с equals? Да, иначе ломается контракт хеш-коллекций.
  4. Чем == отличается от equals? Для ссылок == сравнивает идентичность, equals — заданное классом логическое равенство.
  5. Что генерирует record? В том числе equals и hashCode по компонентам.
  6. Почему изменяемый ключ опасен? После изменения его ищут уже по другому hash.