Как Set определяет уникальность объекта при добавлении в Java
Что проверяют
Знаете ли вы контракт equals() и hashCode() и понимаете ли, что разные реализации Set определяют дубликаты по-разному. Следом обычно спрашивают, что будет, если переопределить только equals(), или просят объяснить поведение TreeSet.
Как отвечать
Сам интерфейс Set лишь обещает отсутствие дубликатов, механизм зависит от реализации.
HashSet внутри держит HashMap, и add(e) сводится к map.put(e, PRESENT):
- корзина: по
hashCode()вычисляется хэш (старшие биты подмешиваются к младшим) и номер корзины; - сравнение: в корзине элементы проверяются сначала по сохранённому хэшу, затем по ссылке
==, и только потом черезequals(); - результат: если нашёлся равный,
addвозвращаетfalseи множество не меняется.
Отсюда контракт: равные по equals() объекты обязаны давать одинаковый hashCode(). Обратное не требуется, коллизии допустимы и лишь замедляют поиск. Когда в корзине больше 8 элементов, а таблица не меньше 64 корзин, цепочка превращается в красно-чёрное дерево. LinkedHashSet работает так же, только помнит порядок вставки.
TreeSet на hashCode() и equals() не смотрит вовсе: дубликат для него это элемент, для которого compareTo() или переданный Comparator вернул 0. Пример расхождения: new BigDecimal("1.0") и new BigDecimal("1.00") не равны по equals(), но compareTo() даёт 0. В HashSet окажутся оба, в TreeSet один.
Для классов-значений удобно брать record: equals() и hashCode() генерируются по всем компонентам.
Где спотыкаются
Переопределяют только equals(). Два «равных» объекта получают разные хэши от Object, попадают в разные корзины, и в множестве оказываются оба.
Меняют поле, от которого зависит hashCode(), уже после добавления. Объект остаётся в старой корзине, contains() его не находит, а remove() не удаляет.
Пишут equals(Person other) вместо equals(Object o). Это перегрузка, а не переопределение, и HashMap её не вызывает. Аннотация @Override поймала бы ошибку на компиляции.