


Если два объекта должны считаться одинаковыми по значению, переопределяйте 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» являются разными значениями по выбранному контракту.
Хеш-коллекция использует hashCode для поиска области хранения, а equals для сопоставления элементов. Если после добавления изменить поле, от которого зависит хеш, поиск может пойти не в ту область. Объект продолжит присутствовать при обходе, но contains или remove могут вести себя неожиданно.
В нашем классе такого изменения нет: String неизменяем, а ссылка final. Для сложного объекта одного final недостаточно: final-ссылка на изменяемый список не делает сам список неизменяемым. Для ключей и элементов множества предпочтительнее устойчивое значение или отдельный неизменяемый идентификатор.
Не пытайтесь устранить коллизии хеша превращением hashCode в случайное число. Коллизии допустимы; нарушение согласованности с equals недопустимо. Также hashCode не является постоянным идентификатором для сохранения в базу и не заменяет криптографическую контрольную сумму.
Для реального доменного класса дополните проверки границами вашего предметного смысла. Автоматически сгенерированные IDE методы полезны, но выбор полей всё равно остаётся решением разработчика.
Такие задачи помогают связать объекты и коллекции на курсе Java с практикой. На разбор с Java-наставником принесите минимальный класс и последовательность add, contains, remove. Она объяснит проблему лучше, чем один снимок содержимого множества.