Найдите баг: объект кладут в HashSet, затем меняют его поле — и contains возвращает false. Объясните и исправьте.
Короткий ответ
- hashCode зависит от изменяемого поля, вошедшего в equals/hashCode
- При вставке объект лёг в корзину по старому хешу
- После мутации contains ищет по новому хешу — в другой корзине
- Объект «потерялся»: он в сете, но не находится
- Исправление: неизменяемые ключи или не мутировать после вставки
- Вариант — удалить из сета, изменить, положить обратно
Ключ хеш-коллекции нельзя мутировать: поиск идёт по новому хешу, а лежит объект по старому.
Как сказать вслух
пример ответаОбъект положили в HashSet — он попал в корзину, вычисленную из хеша на тот момент. Потом поле, которое участвует в hashCode, изменили. Теперь contains считает уже новый хеш и смотрит в другую корзину, где объекта нет. То есть элемент физически в сете, но найти его нельзя, и удалить тоже. Правильное решение — делать ключи неизменяемыми, например record, либо никогда не менять объект, пока он лежит в хеш-коллекции.
Подробный ответ
Основной ответ
HashSet (внутри — HashMap) размещает элемент в корзине по hashCode на момент add. Если после вставки изменить поле, входящее в hashCode, повторный поиск вычислит другой хеш и пойдёт в другую корзину — contains и remove вернут false, итерация при этом объект покажет. Это приводит к утечкам: «неудаляемые» элементы накапливаются. Исправления по убыванию надёжности: сделать ключ неизменяемым (record, final-поля); исключить изменяемые поля из equals/hashCode, строя их на стабильном бизнес-идентификаторе; дисциплина «удалил — изменил — вставил обратно», если мутация неизбежна. Тот же баг существует для ключей HashMap и элементов, по которым построен TreeSet (там ломается порядок сравнения).
Ключевые моменты
- Корзина фиксируется при вставке. Коллекция не пересчитывает хеши лежащих элементов — она не знает о мутации.
- Симптомы. contains == false при «видимом» элементе в итерации, remove не удаляет, дубликаты в сете.
- Исправление. Неизменяемый ключ (record) или hashCode по неизменяемому id.
- Шире, чем HashSet. Та же проблема с ключами HashMap и компараторами TreeMap/TreeSet.
Практический контекст
Классическая задача «найди баг» на лайвкодинге: проверяет связку устройства HashMap и контракта equals/hashCode на живом примере. В реальном коде так теряются объекты в кэшах и реестрах подписчиков. Интервьюер ждёт точного объяснения механики (старая корзина против нового хеша), а не общего «так нельзя», и пары вариантов исправления с оценкой их надёжности.
Пример кода
// Класс с изменяемым полем в equals/hashCode
class User {
String email; // участвует в equals/hashCode
User(String email) { this.email = email; }
@Override public boolean equals(Object o) {
return o instanceof User u && email.equals(u.email);
}
@Override public int hashCode() { return email.hashCode(); }
}
Set<User> users = new HashSet<>();
User u = new User("a@mail.com");
users.add(u);
u.email = "b@mail.com"; // мутация ключа!
System.out.println(users.contains(u)); // false
System.out.println(users.remove(u)); // false — утечка
// Fix: final String email или record User(String email)Частые ошибки
- Объясняют баг «сломался equals», хотя equals корректен — ищут не в той корзине
- Предлагают пересоздавать коллекцию после каждой мутации вместо исправления ключа
- Не замечают, что remove тоже не сработает и элемент утёк навсегда