HashSet: примеры (JAVA)

Примеры работы с множествами
Раздел: Коллекции, Set
HashSet: class

Описание класса HashSet

Класс HashSet представляет реализацию множества на базе хеш-таблицы. Элементы неупорядочены, дублирующие элементы не допускаются. Внутренне HashSet использует HashMap<E, Object> для хранения значений; ключи карты - элементы множества, значение - фиктивный маркер.

Типичные сценарии применения: удаление дубликатов, проверка принадлежности в среднем за O(1), реализация быстрого множества элементов для множества операций (объединение, пересечение, разность).

Основные конструкторы и их аргументы:

  • HashSet() - пустое множество с дефолтной начальной ёмкостью и loadFactor = 0.75f.
  • HashSet(int initialCapacity) - начальная ёмкость карты; IllegalArgumentException при отрицательной ёмкости.
  • HashSet(int initialCapacity, float loadFactor) - начальная ёмкость и коэффициент заполнения; IllegalArgumentException при отрицательной ёмкости или неположительном loadFactor.
  • HashSet(Collection<? extends E> c) - создаёт множество, содержащие все элементы переданной коллекции; NullPointerException при null-параметре.

Частые методы и поведение:

  • boolean add(E e) - добавляет элемент, возвращает true, если элемент отсутствовал; при наличии возвращает false. Допускается null (один null).
  • boolean remove(Object o) - удаляет элемент, возвращает true, если элемент был; иначе false.
  • boolean contains(Object o) - проверка принадлежности.
  • int size() - количество элементов.
  • boolean isEmpty() - проверка пустоты.
  • void clear() - удаляет все элементы.
  • Iterator<E> iterator() - итератор по элементам; итератор обеспечивает быстрый fail-fast: при структурном изменении коллекции извне он бросает ConcurrentModificationException.
  • Object clone() - поверхностная копия множества (новая HashSet с теми же элементами).
  • Object[] toArray() и <T> T[] toArray(T[] a) - преобразование в массив.
  • Методы из интерфейса Collection: addAll, containsAll, removeAll, retainAll, removeIf (с Java 8).
  • С Java 8 доступны stream(), parallelStream(), forEach, spliterator().

Особенности и возвращаемые значения описаны в документации методов: большинство операций возвращают булево значение, отражающее изменение множества. Методы, принимающие коллекцию, выбрасывают NullPointerException при null-аргументе.

Короткие примеры использования

Добавление, проверка, проигнорированные дубликаты и работа с null:

import java.util.HashSet;
public class Main {
  public static void main(String[] args) {
    HashSet<String> s = new HashSet<>();
    System.out.println(s.add("a")); // true
    System.out.println(s.add("a")); // false
    System.out.println(s.add(null));  // true
    System.out.println(s.add(null));  // false
    System.out.println(s.contains("a")); // true
    System.out.println(s.size()); // 2
  }
}
true
false
true
false
true
2

Итерация и удаление через итератор (без ConcurrentModificationException):

import java.util.HashSet;
import java.util.Iterator;
public class Main2 {
  public static void main(String[] args) {
    HashSet<Integer> s = new HashSet<>();
    s.add(1); s.add(2); s.add(3);
    Iterator<Integer> it = s.iterator();
    while (it.hasNext()) {
      Integer v = it.next();
      if (v == 2) it.remove();
    }
    System.out.println(s); // содержит 1 и 3
  }
}
[1, 3]  // порядок не гарантируется

Создание из коллекции и операции над множествами:

import java.util.*;
public class Main3 {
  public static void main(String[] args) {
    List<String> list = Arrays.asList("a","b","a");
    HashSet<String> set = new HashSet<>(list);
    HashSet<String> other = new HashSet<>();
    other.add("b"); other.add("c");
    set.retainAll(other); // пересечение
    System.out.println(set); // [b]
  }
}
[b]

Похожие структуры в Java и их отличия

  • LinkedHashSet - сохраняет порядок вставки, подходит когда важен детерминированный порядок и требуется поведение множества.
  • TreeSet - отсортированное множество на базе красно-черного дерева; операции O(log n); требуется Comparable или Comparator.
  • EnumSet - специализированное множество для enum-типов; очень эффективно по памяти и по скорости, но работает только для enum.
  • CopyOnWriteArraySet - потокобезопасная реализация для редких модификаций и частых чтений; при модификации создаётся копия внутреннего массива.
  • Collections.synchronizedSet или ConcurrentHashMap.newKeySet() - потокобезопасные варианты; первый оборачивает и синхронизирует доступ, второй даёт масштабируемую конкурентную реализацию.

Выбор зависит от требований: порядок (LinkedHashSet), сортировка (TreeSet), производительность для enum (EnumSet), многопоточность (ConcurrentHashMap.newKeySet или CopyOnWriteArraySet).

Аналоги в других языках и особенности

Короткие примеры и отличия от Java:

  • JavaScript - встроенный Set. Хранит объекты по ссылке, допускает один NaN и один undefined. Итерация в порядке вставки.
const s = new Set();
s.add('a'); s.add('a');
console.log(s.size); // 1
1
  • Python - встроенный set. Неупорядочен (в CPython порядок зависит от хешей), поддерживает null-эквивалент None. Для неизменяемых коллекций есть frozenset.
s = set()
s.add('a')
s.add('a')
print(len(s))
1
  • PHP - стандартного типа Set нет; часто используются ассоциативные массивы с ключами-элементами: $a[$value]=true, или расширение Ds\Set.
$s = [];
$s['a'] = true;
$s['a'] = true;
echo count($s); // 1
1
  • C# - HashSet<T>, поведение похоже на Java HashSet; поддерживает null для ссылочных типов.
var s = new System.Collections.Generic.HashSet<string>();
s.Add("a"); s.Add("a");
Console.WriteLine(s.Count); // 1
1
  • Go - нет встроенного set; распространённый приём: map[T]struct{}.
s := make(map[string]struct{})
s["a"] = struct{}{}
s["a"] = struct{}{}
fmt.Println(len(s)) // 1
1
  • Lua - таблицы используются как множества: t[v] = true.
s = {}
s['a'] = true
s['a'] = true
print(#s) -- # не даёт корректный размер, используется перебор
local count = 0
for k in pairs(s) do count = count + 1 end
print(count) -- 1
1

Отличия: в Java HashSet деталями управления хешами и поведением при коллизиях занимается HashMap; в других языках реализация и гарантии (порядок, поддержка null, сравнение объектов) отличаются.

Типичные ошибки и примеры

1) Ожидание сохранённого порядка. Итерация по HashSet не гарантирует порядок; возможные неожиданные результаты при сравнении коллекций с разным порядком.

HashSet<Integer> s = new HashSet<>();
s.add(3); s.add(1); s.add(2);
System.out.println(s); // порядок может быть [1, 2, 3] или иной
[1, 2, 3]  // пример, но не гарантия

2) Изменение полей объекта, участвующих в equals/hashCode, после добавления в HashSet. Это ломает поведение contains/remove.

import java.util.HashSet;
class P { int id; P(int id){this.id=id;} public int hashCode(){return id;} public boolean equals(Object o){return (o instanceof P) && ((P)o).id==id;}}
public class Test { public static void main(String[] a){
  HashSet<P> s = new HashSet<>();
  P p = new P(1); s.add(p);
  System.out.println(s.contains(new P(1))); // true
  p.id = 2; // изменена часть хеш-кода
  System.out.println(s.contains(new P(1))); // может быть false
}}
true
false

3) ConcurrentModificationException при модификации множества вне итератора во время прохода.

HashSet<Integer> s = new HashSet<>();
s.add(1); s.add(2);
for (Integer x : s) {
  s.add(3); // вызовет ConcurrentModificationException
}
Exception in thread "main" java.util.ConcurrentModificationException
 at java.base/java.util.HashMap$KeyIterator.next(HashMap.java:xxxx)
 ...

4) Неправильные аргументы конструктора: отрицательная начальная ёмкость или неположительный loadFactor приводят к IllegalArgumentException.

new HashSet<String>(-1); // IllegalArgumentException
Exception in thread "main" java.lang.IllegalArgumentException: Illegal initial capacity: -1

Изменения в поведении и API в последних версиях Java

  • С Java 8 появились удобства для коллекций: stream(), removeIf, forEach, что влияет на способы обработки HashSet без явных итераторов.
  • В реализации HashMap (на которой основан HashSet) с Java 8 введены красно-черные деревья в корзинах при больших коллизиях для улучшения производительности; это влияет на поведение при атакующих хеш-функциях и на сложность операций.
  • Начиная с Java 9 добавлены фабричные методы для множеств: Set.of(...) создаёт неизменяемое множество - альтернатива для небольших фиксированных наборов.
  • Сам API HashSet не претерпел кардинальных изменений, но добавленные методы в интерфейсах коллекций расширяют возможности использования HashSet в функциональном стиле.

Расширенные примеры и нетипичные сценарии

Удаление дубликатов по полю объекта с помощью HashSet и кастомного equals/hashCode:

Пример java
import java.util.*;
class User { String name; int age; User(String n,int a){name=n;age=a;} 
  public boolean equals(Object o){ return (o instanceof User) && name.equals(((User)o).name); }
  public int hashCode(){ return name.hashCode(); }
  public String toString(){ return name+":"+age; }
}
public class Dedupe { public static void main(String[] args){
  List<User> list = Arrays.asList(new User("a",10), new User("b",20), new User("a",30));
  HashSet<User> set = new HashSet<>(list);
  System.out.println(set); // оставит одного пользователя с именем 'a'
}}
[a:10, b:20]  // один из дубликатов с именем 'a' остаётся, какой именно зависит от порядка вставки

Получение уникальных по свойству через Stream и вспомогательный HashSet:

Пример java
import java.util.*;
import java.util.stream.*;
public class UniqueBy {
  public static void main(String[] args){
    List<String> list = Arrays.asList("a1","a2","b1","a1");
    Set<String> seen = new HashSet<>();
    List<String> unique = list.stream()
      .filter(s -> seen.add(s.substring(0,1))) // сохраняет по первой букве
      .collect(Collectors.toList());
    System.out.println(unique); // [a1, b1]
  }
}
[a1, b1]

Identity set - множество по идентичности объектов (использование IdentityHashMap):

Пример java
import java.util.*;
public class IdentitySetExample {
  public static void main(String[] args){
    Set<String> identitySet = Collections.newSetFromMap(new IdentityHashMap<>());
    String a = new String("x");
    String b = new String("x");
    identitySet.add(a); identitySet.add(b);
    System.out.println(identitySet.size()); // 2, так как сравнение по ссылке
  }
}
2

Оптимизация производительности: указание начальной ёмкости при загрузке большого объёма данных для снижения количества расширений:

Пример java
int expected = 1_000_000;
HashSet<Integer> big = new HashSet<>(expected * 2); // запас по ёмкости
// затем массовая вставка
(практический эффект: уменьшение количества перераспределений и затрат на rehash)

Использование HashSet для быстрых множественных операций (объединение/пересечение/разность) и измерение сложности по памяти и времени - полезно для работы с большими наборами данных, при этом важно учитывать нагрузочный фактор и возможные коллизии.

джава HashSet function comments

En
HashSet Реализация Set на основе хэш-таблицы