Какой контракт между методами hashCode() и equals() должен соблюдаться для корректной работы ключей в HashMap?

Ответ

Для корректной работы объекта в качестве ключа HashMap между его методами hashCode() и equals() должен соблюдаться следующий контракт:

  1. Согласованность: Если a.equals(b) возвращает true, то a.hashCode() должен быть равен b.hashCode().
  2. Обратное не обязательно: Равные хеш-коды (a.hashCode() == b.hashCode()) не гарантируют, что a.equals(b) == true. Это коллизия.
  3. Неизменность: Значения, возвращаемые hashCode() и результат equals(), должны быть постоянными для объекта на протяжении его жизни (при условии, что поля, участвующие в сравнении, не изменяются).

Почему это важно для HashMap? При добавлении элемента map.put(key, value):

  1. Вычисляется hashCode() ключа для определения индекса корзины (bucket).
  2. Если в корзине уже есть элементы (коллизия), для каждого из них вызывается equals(key) для проверки, является ли ключ дубликатом.

Нарушение контракта приводит к ошибкам:

  • Если equals говорит, что ключи одинаковы, но hashCode разный, дубликат ключа будет помещен в другую корзину, что нарушит уникальность ключей.
  • Объект, помещенный в мапу как ключ, после изменения своих полей может стать «ненаходимым», так как изменится его hashCode().

Пример корректной реализации:

public class Person {
    private final String id;
    private String name;

    public Person(String id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return Objects.equals(id, person.id); // Сравниваем только по id
    }

    @Override
    public int hashCode() {
        return Objects.hash(id); // Хешируем только по id
    }
}

Ответ 18+ 🔞

А, ну это же классика, блядь! Сейчас объясню, как эти два закадычных метода в HashMap друг за друга горой стоят, а если один подведёт — пиши пропало, всё наебнётся.

Представь себе, hashCode() и equals() — это как два мудака на стройке. Один (hashCode) кричит: «Эй, тащите этот ящик на третий этаж, в секцию «Б»!». А второй (equals) уже там, на третьем этаже, встречает и проверяет: «Так, стоп, а этот ящик точно такой же, как тот, что уже лежит? Или другой?».

И вот их священные уговоры, контракт, блядь:

  1. Главное правило, ебать его в сраку: Если equals сказал, что два объекта — одно и то же говно (a.equals(b) == true), то и hashCode у них должен быть одинаковый, как сиамские близнецы. Иначе HashMap просто обосрётся и положит «одинаковый» ключ в другую корзину, и ты его потом никогда не найдёшь. Полный пиздец с уникальностью.

  2. А вот наоборот — не факт, блядь. Если хеш-коды совпали (a.hashCode() == b.hashCode()), это ещё не значит, что объекты равны. Это просто коллизия, два разных ящика в одну секцию засунули. Ну бывает, ёпта. Тогда уже equals придёт и разберётся, кто есть кто.

  3. И главное — не шевелись, сука! Пока объект лежит в мапе ключом, его hashCode() и результат equals() должны быть как скала — неизменны. А то представь: положил ты объект, всё хорошо. Потом ты ему поля поменял, хеш-код уехал в другую вселенную. Ты его потом искать будешь по старому хешу — а его там уже нет! Объект как в воду канул, хотя мапа трясёшь — он там где-то есть. Волшебство, блядь, да и только.

Короче, если нарушить этот контракт, HashMap превратится из умной структуры в кучу беспорядочного мусора, который ничего не находит и всё дублирует. Хуйня полная.

Вот смотри, как надо делать по-человечески, чтобы не было потом мучительно больно:

public class Person {
    private final String id; // Вот это поле — наше всё, по нему и сравниваем, и хешируем
    private String name; // А это можно менять, оно в equals/hashCode не участвует

    public Person(String id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // Один и тот же объект? Ну ясень пень, true
        if (o == null || getClass() != o.getClass()) return false; // Сравниваем только с себе подобными
        Person person = (Person) o;
        return Objects.equals(id, person.id); // Всё решает id, блядь! Только он!
    }

    @Override
    public int hashCode() {
        return Objects.hash(id); // И хеш-код тупо от id же! Красота, один хуй в пальто.
    }
}

Вот так, сука, и живём. Согласованно. А то накосячишь с этими методами — потом дебажи, ищи ветра в поле.