Карта / Коллекции / equals / hashCode

equals / hashCode

equals() решает, равны ли два объекта по содержимому. hashCode() даёт объекту “номер ячейки” для хэш-коллекций. По умолчанию (без переопределения) оба метода сравнивают объекты по ссылке — как ==:

class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
}

Point a = new Point(1, 2);
Point b = new Point(1, 2);
a.equals(b);   // false! разные объекты, хоть и с одинаковыми полями

Чтобы “одинаковые по смыслу” объекты считались равными, equals()hashCode() вместе с ним) нужно переопределить:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Point p)) return false;
    return x == p.x && y == p.y;
}

@Override
public int hashCode() {
    return Objects.hash(x, y);
}
Копнуть глубже

Главное правило-контракт: если equals() говорит “равны”, hashCode() обязан вернуть одно и то же число для обоих. Обратное необязательно — у разных объектов хэш может совпасть (коллизия), это нормально. Но равные объекты с разным хэшем — это баг, который незаметно ломает HashMap/HashSet.

Set<Point> points = new HashSet<>();
points.add(new Point(1, 2));
points.contains(new Point(1, 2));   // false без правильного equals+hashCode!

Если переопределить только equals(), но не hashCode()HashSet ищет элемент не в той корзине (по старому, унаследованному хэшу) и просто не находит его, хотя по смыслу элемент “там есть”.

IDE генерирует оба метода автоматически (правый клик → Generate → equals and hashCode) — почти никто не пишет их руками, но понимать, что они делают и зачем, обязательно.

Под капотом

Этот контракт — не просто хорошая практика, а то, на чём буквально держится механизм хэш-таблиц. HashMap/HashSet сначала находят корзину по hashCode(), а внутри корзины различают записи через equals() (подробнее — в теме «Устройство HashMap»). Если хэш не совпадает у “равных” объектов — поиск идёт в неправильную корзину и просто не находит элемент, даже если он формально есть в коллекции.

record (см. тему «record для DTO») генерирует корректные equals()/hashCode() автоматически, на основе всех полей — ещё одна причина, почему он удобен для простых классов-данных.

🎤 Закрыл тему, если можешь объяснить:
• почему два объекта с одинаковыми полями по умолчанию не равны;
• контракт equals/hashCode и почему нарушение ломает HashSet (если дошёл до 2-го слоя).