W3docs

Интерфейс Set в Java

Коллекции уникальных элементов в Java: интерфейс Set и его основные реализации.

Set<E> — это Collection<E> с одним дополнительным правилом: без дубликатов. Сам интерфейс почти не добавляет новых методов — он наследует add, remove, contains, size, iterator и остальные. Меняется только то, что обещают эти методы: add(e) возвращает false (и ничего не изменяет), если равный элемент уже есть в множестве; два множества equals, если содержат одинаковые элементы независимо от порядка; hashCode — это сумма хешей элементов, поэтому равные множества всегда дают одинаковый результат.

Этот простой контракт — «элементы уникальны» — оказывается именно тем, что нужно для проверки вхождения, дедупликации, пересечения/объединения и тысячи других паттернов, которые неудобно реализовывать через List.

Что считается дубликатом

Set определяет дубликаты по equals (а в хеш-based множествах — ещё и по hashCode как функции разбиения по бакетам). Это не сравнение по ссылке и не «выглядит одинаково при выводе». Если вы помещаете свой класс в Set, необходимо переопределить оба метода вместе; контракт рассматривается в главе equals and hashCode.

Set<String> tags = new HashSet<>();
tags.add("java");
tags.add("java");      // add returns false; the set still has one element
tags.add(new String("java")); // also false — equals, not reference

Распространённая ловушка: хранить изменяемые элементы и модифицировать их, пока они находятся в хеш-based множестве. Хеш элемента меняется, множество ищет его в неправильном бакете, и contains начинает давать неверный результат. Правило: если вы поместили объект в хеш-множество, считайте его эффективно неизменяемым с этого момента. У TreeSet та же ловушка, но с compareTo.

Четыре стандартные реализации

В java.util входят четыре реализации Set, покрывающие почти все сценарии использования:

КлассВнутренняя структураПорядокNull-элементыТипичное применение
HashSetхеш-таблицаотсутствуетодин null допускаетсяпо умолчанию — быстрейшие проверки вхождения
LinkedHashSetхеш-таблица + двусвязный списокпорядок вставкиодин null допускаетсякогда порядок итерации должен совпадать с порядком вставки
TreeSetкрасно-чёрное деревоестественный / компараторный порядокnull не допускаетсякогда нужна сортированная итерация или диапазонные запросы
EnumSetбитовый векторпорядок объявления enumnull не допускаетсядля Set<MyEnum> — компактный, быстрый, упорядоченный

Первые три реализации рассматриваются в следующих трёх главах; EnumSet разбирается позже вместе с EnumMap.

Массовые операции: объединение, пересечение, разность

Set наследует четыре массовые операции из Collection, и на множестве они обретают классические теоретико-множественные значения:

  • addAll(other)объединение (на месте). После вызова a содержит все элементы обеих сторон.
  • retainAll(other)пересечение (на месте). После вызова a содержит только элементы, присутствовавшие и в other.
  • removeAll(other)разность (на месте). После вызова a содержит только элементы, которых не было в other.
  • containsAll(other)проверка подмножества. Возвращает true, если каждый элемент other есть в a.

Эти операции изменяют получатель. Если нужна неразрушающая версия, сначала скопируйте: Set<E> u = new HashSet<>(a); u.addAll(b);.

Равенство и хеш-коды множеств

Контракт Set для equals необычен: два множества равны, если содержат одинаковые элементы, независимо от порядка или типа реализации. HashSet, LinkedHashSet и TreeSet с одинаковыми элементами считаются равными друг другу.

Set<Integer> a = new HashSet<>(List.of(1, 2, 3));
Set<Integer> b = new TreeSet<>(List.of(3, 2, 1));
System.out.println(a.equals(b));   // true

Именно поэтому массовые операции и фабричные методы неизменяемых коллекций могут свободно работать с разными реализациями — соблюдается лишь правило «одинаковые элементы».

Фабрики для read-only и неизменяемых множеств

Начиная с Java 9 самый простой способ создать небольшое фиксированное множество — Set.of(...):

Set<String> primary = Set.of("red", "green", "blue");

Set.of возвращает неизменяемое множество, которое не допускает null-элементов и выбрасывает исключение, если при создании переданы дубликаты. Для защитного снимка существующего множества используйте Set.copyOf(existing) — тоже неизменяемое, тоже не принимает null.

Для представления, скрывающего мутации (исходное множество можно изменять, но через представление добавлять нельзя), используйте Collections.unmodifiableSet(s). Глава о неизменяемых коллекциях в этой части расскажет, когда что выбирать.

Практический пример: дедупликация, алгебра множеств и порядок

Программа ниже использует все четыре реализации, демонстрирует массовые операции на реальных данных и показывает, как различается порядок итерации у HashSet, LinkedHashSet и TreeSet.

java— editable, runs on the server

Что стоит отметить по результатам выполнения:

  • add вернул false для каждого дубликата \"java\" — вот как написать дедупликатор в две строки.
  • Объединение, пересечение и разность — это один вызов addAll/retainAll/removeAll каждый — копируйте сначала, если не хотите потерять оригинал.
  • Три реализации содержат одинаковые элементы и считаются равными, но порядок итерации у них очень разный. Выбирайте реализацию по нужному порядку, а не по привычке.

Что дальше

Наиболее распространённая реализация Set — основанная на хеш-таблице, и именно к ней обращаются по умолчанию. Следующая глава посвящена HashSet: мы рассмотрим коэффициент загрузки, перехеширование и что означает «почти константное время» на практике.

Практика

Практика
Что возвращает `set.add(x)`, если `x` уже является элементом `set`, согласно контракту `Set`?
Что возвращает `set.add(x)`, если `x` уже является элементом `set`, согласно контракту `Set`?
Was this page helpful?