Интерфейс Set, классы HashSet и LinkedHashSet в Java - Вопросы
Всего: 6 вопросов
1. Что такое интерфейс Set в Java и какие у него основные реализации?
Что такое интерфейс Set в Java и какие у него основные реализации?
Set (java.util.Set<E>) — интерфейс множества: каждый элемент хранится не больше одного раза, доступа по индексу нет. Он расширяет Collection и не добавляет новых абстрактных методов, но уточняет контракт: add() уже существующего элемента ничего не меняет и возвращает false.
Основные реализации:
HashSet— хеш-таблица, порядок обхода не гарантирован, элементы сравниваются черезhashCode()иequals();LinkedHashSet— хеш-таблица плюс двусвязный список, порядок вставки;TreeSet— хранит элементы отсортированными и сравнивает их черезcompareTo()илиComparator.
Кроме того, есть статические фабрики Set.of(...) (Java 9) и Set.copyOf(...) (Java 10): они создают неизменяемое множество, add() бросает UnsupportedOperationException, null запрещён, а повторяющийся аргумент в Set.of("a", "a") бросает IllegalArgumentException.
2. Как HashSet устроен внутри и как он находит элемент?
Как HashSet устроен внутри и как он находит элемент?
Внутри HashSet лежит HashMap: элементы множества хранятся как его ключи, а значением для всех ключей служит один общий объект-заглушка. Поэтому для элементов действуют те же правила, что для ключей HashMap, и допускается один null.
Поиск и добавление идут в два шага:
- по
hashCode()элемента вычисляется номер бакета (ячейки таблицы):(h ^ (h >>> 16)) & (capacity - 1); - если в бакете несколько элементов (коллизия), нужный выбирается методом
equals().
Отсюда два следствия: HashSet зависит от обоих методов, а элементы обходятся в порядке бакетов — не в порядке вставки и не по возрастанию. null всегда попадает в бакет 0, а после рехеширования порядок обхода может измениться.
3. Каков контракт методов hashCode() и equals() и что будет с HashSet, если его нарушить?
Каков контракт методов hashCode() и equals() и что будет с HashSet, если его нарушить?
Контракт из документации Object:
- пока не изменились поля, участвующие в
equals(),hashCode()объекта возвращает одно и то же число (в рамках одного запуска); - если
a.equals(b), тоa.hashCode() == b.hashCode(); - обратное неверно: одинаковые хеш-коды не означают равенства (это коллизия);
- следовательно, при разных хеш-кодах объекты точно не равны.
Если переопределить только equals(), «равные» объекты получат разные хеш-коды из Object, попадут в разные бакеты, и equals() даже не вызовется: множество примет дубликат (size() вернёт 2), а contains() для равного объекта вернёт false.
hashCode(), возвращающий константу, контракт не нарушает, но все объекты окажутся в одном бакете и поиск станет линейным. Оба метода удобно генерировать в IntelliJ IDEA, собирать хеш через Objects.hash(...) или использовать record (Java 16), где они создаются автоматически.
4. Что такое initialCapacity и loadFactor у HashSet и когда происходит рехеширование?
Что такое initialCapacity и loadFactor у HashSet и когда происходит рехеширование?
initialCapacity — начальное число бакетов хеш-таблицы (по умолчанию 16). Ёмкость всегда округляется вверх до степени двойки: new HashSet<>(100) создаёт таблицу на 128 бакетов.
loadFactor — допустимая заполненность таблицы (по умолчанию 0,75). Когда число элементов становится больше capacity * loadFactor, происходит рехеширование: создаётся таблица вдвое больше, и все элементы раскладываются заново. Для 16 и 0,75 это 13-й элемент, для new HashSet<>(100) — 97-й.
Больший коэффициент экономит память, но увеличивает число коллизий; меньший ускоряет поиск ценой памяти.
Чтобы заранее выделить место ровно под N элементов, на Java 19+ используют HashSet.newHashSet(N) (для LinkedHashSet — LinkedHashSet.newLinkedHashSet(N)), а на старых версиях передают ёмкость (int) (N / 0.75f) + 1.
5. Какова сложность операций HashSet и что происходит в худшем случае?
Какова сложность операций HashSet и что происходит в худшем случае?
add(), contains() и remove() выполняются в среднем за O(1), если хеш-коды распределены равномерно. size() и isEmpty() всегда O(1): размер хранится в отдельном поле.
Худший случай — все элементы в одном бакете (например, hashCode() возвращает константу):
- до Java 8 бакет был связным списком, поиск — O(n);
- с Java 8 бакет из 8 и более элементов превращается в красно-чёрное дерево (если в таблице не меньше 64 бакетов, иначе таблица просто увеличивается), поиск — O(log n); лучше всего это работает, если элементы реализуют
Comparable.
Перебор HashSet занимает время, пропорциональное сумме размера и ёмкости, потому что итератор проходит и пустые бакеты. У LinkedHashSet перебор пропорционален только размеру.
6. Чем LinkedHashSet отличается от HashSet и какие методы он получил в Java 21?
Чем LinkedHashSet отличается от HashSet и какие методы он получил в Java 21?
LinkedHashSet — подкласс HashSet, который внутри использует LinkedHashMap и связывает элементы двусвязным списком. Поэтому он гарантирует порядок вставки, а HashSet не гарантирует никакого порядка. Операции остаются O(1) в среднем, но каждый элемент занимает больше памяти (ссылки на предыдущий и следующий), а вставка и удаление немного медленнее.
Повторный add() существующего элемента не меняет его позицию. Типичное применение — убрать дубликаты из списка, сохранив порядок: new ArrayList<>(new LinkedHashSet<>(list)).
В Java 21 LinkedHashSet реализует SequencedSet и получил методы getFirst(), getLast(), addFirst(), addLast(), removeFirst(), removeLast() и reversed(). В отличие от add(), addFirst()/addLast() перемещают уже существующий элемент в начало или конец. Переменная должна иметь тип LinkedHashSet или SequencedSet: у Set этих методов нет.