HashSet: примеры (JAVA)
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:
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:
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):
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
Оптимизация производительности: указание начальной ёмкости при загрузке большого объёма данных для снижения количества расширений:
int expected = 1_000_000;
HashSet<Integer> big = new HashSet<>(expected * 2); // запас по ёмкости
// затем массовая вставка
(практический эффект: уменьшение количества перераспределений и затрат на rehash)
Использование HashSet для быстрых множественных операций (объединение/пересечение/разность) и измерение сложности по памяти и времени - полезно для работы с большими наборами данных, при этом важно учитывать нагрузочный фактор и возможные коллизии.