Equals и hashCode в Java: почему HashSet не узнаёт объект

Equals и hashCode в Java: почему HashSet не узнаёт объект
Содержание статьи

Если два объекта должны считаться одинаковыми по значению, переопределяйте equals и hashCode согласованно. Для равных объектов хеш-код обязан совпадать. Обратное неверно: одинаковый хеш ещё не доказывает равенство.

На Mentoring.digital рассмотрим артикул товара как учебный объект. Нас интересует не устройство всей коллекции, а конкретный контракт: два экземпляра с одним артикулом должны занимать одно место в HashSet.

Отделите экземпляр от значения

Оператор == для ссылок проверяет, указывают ли они на один объект. Два вызова new Sku("BOOK-01") создают разные экземпляры. Но по условию нашего примера они обозначают один артикул и должны быть равны по equals.

Унаследованный Object.equals использует равенство ссылок. Поэтому без собственного определения равенства коллекция не обязана объединять объекты с одинаковыми полями. Сначала решите, какие поля определяют сущность: случайное включение описания или отображаемого имени может изменить смысл равенства.

Реализуйте оба метода на одном наборе полей

import java.util.HashSet;
import java.util.Objects;

public class SkuDemo {
    static final class Sku {
        private final String value;
        Sku(String value) {
            this.value = Objects.requireNonNull(value);
        }
        @Override
        public boolean equals(Object other) {
            if (this == other) return true;
            if (!(other instanceof Sku)) return false;
            Sku that = (Sku) other;
            return value.equals(that.value);
        }
        @Override
        public int hashCode() {
            return value.hashCode();
        }
    }

    public static void main(String[] args) {
        Sku first = new Sku("BOOK-01");
        Sku second = new Sku("BOOK-01");
        assert first != second;
        assert first.equals(second);
        assert first.hashCode() == second.hashCode();
        var items = new HashSet<Sku>();
        items.add(first);
        items.add(second);
        assert items.size() == 1;
        assert items.contains(new Sku("BOOK-01"));
        System.out.println("OK");
    }
}

Сохраните пример в SkuDemo.java. В окружении с JDK выполните javac SkuDemo.java, затем java -ea SkuDemo. Флаг -ea включает assert. Без него программа могла бы вывести OK, вообще не выполнив эти проверки.

Класс final не допускает наследников с другим определением равенства. Поле value неизменяемое и не может быть null. Сравнение и хеширование опираются на одну строку. Здесь регистр и пробелы значимы: «BOOK-01» и «book-01» являются разными значениями по выбранному контракту.

Что ломается при изменении объекта внутри HashSet

Хеш-коллекция использует hashCode для поиска области хранения, а equals для сопоставления элементов. Если после добавления изменить поле, от которого зависит хеш, поиск может пойти не в ту область. Объект продолжит присутствовать при обходе, но contains или remove могут вести себя неожиданно.

В нашем классе такого изменения нет: String неизменяем, а ссылка final. Для сложного объекта одного final недостаточно: final-ссылка на изменяемый список не делает сам список неизменяемым. Для ключей и элементов множества предпочтительнее устойчивое значение или отдельный неизменяемый идентификатор.

Не пытайтесь устранить коллизии хеша превращением hashCode в случайное число. Коллизии допустимы; нарушение согласованности с equals недопустимо. Также hashCode не является постоянным идентификатором для сохранения в базу и не заменяет криптографическую контрольную сумму.

Как проверить собственный класс

  • Объект равен самому себе, но не null.
  • Два значения сравниваются симметрично, а цепочка равных значений сохраняет транзитивность.
  • Повторные сравнения дают один результат, пока значимые данные не изменены.
  • Равные экземпляры имеют одинаковый hashCode и обнаруживаются в HashSet.
  • Разные значения не объявляются равными только из-за совпадения хеша.

Для реального доменного класса дополните проверки границами вашего предметного смысла. Автоматически сгенерированные IDE методы полезны, но выбор полей всё равно остаётся решением разработчика.

Такие задачи помогают связать объекты и коллекции на курсе Java с практикой. На разбор с Java-наставником принесите минимальный класс и последовательность add, contains, remove. Она объяснит проблему лучше, чем один снимок содержимого множества.

Похожие статьи по данной теме