Set: примеры (JAVA)
Set: interfaceЧто такое Set и когда применяется
Интерфейс Set в Java представляет коллекцию элементов без дубликатов. Используется, когда нужно хранить уникальные значения и проверять принадлежность быстро. Поведение конкретной реализации зависит от класса: порядок элементов может быть неопределён, предсказуем (вставочный) или отсортирован.
Основные контрактные свойства:
- Отсутствие дубликатов: добавление элемента, равного уже существующему по equals, не изменит множество.
- Равенство множеств определяется по элементам, порядок не обязателен.
Типичные реализации и их особенности:
- HashSet<E> - основан на хеш-таблице, быстрые операции добавления/поиска/удаления, не гарантирует порядок, допускает null.
- LinkedHashSet<E> - сохраняет порядок вставки, чуть более затратен по памяти, допускает null.
- TreeSet<E> - отсортированное множество, элементы должны реализовывать Comparable или использовать Comparator; не допускает null при естественном порядке.
- EnumSet<E extends Enum<E>> - высокоэффективное множество для перечислений, работа через биты.
- CopyOnWriteArraySet<E> и множества на базе ConcurrentHashMap - для многопоточной среды.
Методы интерфейса и их аргументы/возвраты
Ниже перечислены ключевые методы интерфейса Set (унаследованные от Collection) с описанием аргументов и возвращаемых значений:
boolean add(E e)- пытается добавить элемент. Возвращаетtrue, если элемент добавлен,false, если аналогичный элемент уже присутствовал.boolean remove(Object o)- удаляет один эквивалентный элемент. Возвращаетtrue, если удаление прошло успешно.boolean contains(Object o)- проверяет наличие эквивалентного элемента. Возвращаетtrueпри наличии.int size()- количество элементов в множестве.boolean isEmpty()- true, если множество пусто.Iterator<E> iterator()- итератор по элементам. При модификации структуры множества вне итератора может бросатьсяConcurrentModificationExceptionв небезопасных реализациях.boolean addAll(Collection<? extends E> c)- добавляет все элементы из коллекции. Возвращает true, если множество изменилось.boolean removeAll(Collection<?> c)- удаляет все элементы, присутствующие в c. Возвращает true при изменении.boolean retainAll(Collection<?> c)- оставляет только элементы, присутствующие в c. Возвращает true при изменении.void clear()- удаляет все элементы; возвращаемого значения нет.boolean containsAll(Collection<?> c)- проверяет, содержатся ли все элементы c; возвращает boolean.Object[] toArray()и<T> T[] toArray(T[] a)- копирование в массив; при втором варианте возвращается массив типа T[].
Особенности аргументов и значений:
- Методы принимают
Objectдля поиска/удаления: это позволяет проверять наличие по equals без необходимости совпадения обобщённого типа. - Некоторые реализации возвращают неизменяемые множества (например,
Set.of), у которых методы модификации кидаютUnsupportedOperationException. - Некоторые реализации не допускают null (например,
TreeSetпри естественном сравнении или неизменяемые множества из фабрик).
Короткие примеры основных вариантов
Пример с HashSet: добавление, проверка и результат вызовов add()
import java.util.*;
public class HashSetExample {
public static void main(String[] args) {
Set s = new HashSet<>();
System.out.println(s.add("a")); // true
System.out.println(s.add("a")); // false
System.out.println(s.contains("a")); // true
System.out.println(s.size()); // 1
}
}
true false true 1
LinkedHashSet сохраняет порядок вставки
import java.util.*;
class LinkedHashSetExample {
public static void main(String[] args) {
Set s = new LinkedHashSet<>();
s.add(3); s.add(1); s.add(2);
for (int x : s) System.out.print(x + " ");
}
}
3 1 2
TreeSet с сортировкой (естественный порядок)
import java.util.*;
class TreeSetExample {
public static void main(String[] args) {
Set s = new TreeSet<>();
s.add("b"); s.add("a"); s.add("c");
System.out.println(s); // [a, b, c]
}
}
[a, b, c]
Неизменяемое множество через фабрику (Java 9+)
import java.util.*;
class ImmutableSetExample {
public static void main(String[] args) {
Set s = Set.of("x", "y");
System.out.println(s.contains("x"));
// s.add("z"); // UnsupportedOperationException
}
}
true
Похожие интерфейсы и классы в Java
Схожие решения в стандартной библиотеке и их особенности:
- Collection - общий суперинтерфейс для Set. При необходимости использовать структуру без семантики уникальности стоит выбирать List или Queue.
- List - хранит порядок и допускает дубликаты; предпочтительнее, когда важен индекс или порядок с дубликатами.
- Map (ключи) - при необходимости хранить пары ключ-значение или учитывать счётчики; ключи Map уникальны, поэтому keySet() похож на Set.
- SortedSet и NavigableSet - расширения Set с упорядочиванием и дополнительными навигационными методами; предпочтительны для задач со сравнением и диапазонами.
- Multiset (Guava) - похож на Set, но позволяет учитывать кратность элементов; используется, когда важен счёт каждой копии.
Аналоги в других языках и отличия от Java
Краткие примеры и особенности в популярных языках:
- JavaScript: встроенный класс
Set. Не допускает дубликатов, использует сравнение по ===. Пример:
const s = new Set();
s.add('a');
s.add('a');
console.log(s.size);
1
- Python: встроенный тип
set. Быстрый, допускает только хешируемые объекты, поддерживает операции union/intersection/difference.
s = set()
s.add(1)
s.add(1)
print(len(s))
1
- C#:
HashSet<T>. Похож на Java HashSet, использует GetHashCode/Equals. Пример:
using System;
using System.Collections.Generic;
class Program {
static void Main() {
var s = new HashSet();
s.Add("a"); s.Add("a");
Console.WriteLine(s.Count);
}
}
1
- Go: нет встроенного Set, обычно используют
map[T]boolкак множество.
package main
import "fmt"
func main() {
s := make(map[string]bool)
s["a"] = true
s["a"] = true
fmt.Println(len(s))
}
1
- Lua: множества через таблицы: ключи - элементы множества.
s = {}
s["a"] = true
s["a"] = true
print(#s) -- неподходящий способ, надо считать вручную
0 (неоднозначно, в Lua длина таблицы не отражает множество)
- PHP: нет встроенного Set в старых версиях; можно использовать массивы с ключами или
SplObjectStorageдля объектов.
$s = [];
$s['a'] = true;
$s['a'] = true;
echo count($s);
1
Отличия от Java:
- В Java у Set есть разные реализации со строгими контрактами equals/hashCode; в динамичных языках (JS, Python) равенство и хеширование реализовано по-другому.
- Во многих языках нет встроенного отсортированного множества, аналог TreeSet обеспечивается библиотеками.
- Некоторые языки (Go, Lua, PHP) используют структуры или соглашения для имитации множества.
Типичные ошибки и примеры
Частые проблемы при использовании Set и примеры:
- Мутация объектов, используемых в HashSet, после вставки. Это ломает контракт хеширования и делает элемент «невидимым» для поиска.
import java.util.*;
class P {
int x;
P(int x){this.x=x;}
public int hashCode(){return x;}
public boolean equals(Object o){return o instanceof P && ((P)o).x==x;}
}
class MutateExample {
public static void main(String[] args){
Set s = new HashSet<>();
P p = new P(1);
s.add(p);
p.x = 2; // сломает поиск
System.out.println(s.contains(new P(1))); // false
System.out.println(s.contains(new P(2))); // false или непредсказуемо
}
}
false false
- ConcurrentModificationException при модификации множества во время итерации (для небезопасных реализаций).
import java.util.*;
class CMEExample {
public static void main(String[] args){
Set s = new HashSet<>(Arrays.asList(1,2,3));
for (Integer i : s) {
if (i==2) s.remove(i); // ConcurrentModificationException
}
}
}
Exception in thread "main" java.util.ConcurrentModificationException at ...
- Использование TreeSet с несравнимыми элементами - ClassCastException или NPE при null.
import java.util.*;
class TreeError {
public static void main(String[] args) {
Set
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer at ...
- Попытка модификации неизменяемого множества, созданного через Set.of()
Set s = Set.of("a");
s.add("b");
Exception in thread "main" java.lang.UnsupportedOperationException at ...
Недавние изменения и полезные дополнения
Эволюция функциональности Set в последних версиях Java:
- Java 8: добавлены Stream API и методы по умолчанию в Collection, например
removeIf,stream(). Стало удобнее выполнять операции множеств с помощью потоков. - Java 9: введены фабричные методы
Set.of(...)и другие неизменяемые коллекции; они удобны для создания небольших неизменяемых наборов, но не допускают null. - Java 10: добавлен метод
Set.copyOf(Collection)для получения неизменяемой копии коллекции (реализация может возвращать исходный объект при отсутствии изменений). - Последние версии продолжают улучшать производительность реализаций коллекций и оптимизации в JDK, но семантика интерфейса Set остаётся стабильной.
Расширенные и редкие сценарии применения
Несколько продвинутых примеров с пояснениями.
1) EnumSet - компактное и быстрое множество для перечислений
import java.util.*;
enum Day {MON, TUE, WED, THU, FRI, SAT, SUN}
class EnumSetExample {
public static void main(String[] args) {
EnumSet work = EnumSet.range(Day.MON, Day.FRI);
System.out.println(work);
}
}
[MON, TUE, WED, THU, FRI]
Пояснение: EnumSet использует битовые операции, минимизирует память и быстрый в сравнении с HashSet для enum-типа.
2) Concurrent set через ConcurrentHashMap
import java.util.*;
import java.util.concurrent.*;
class ConcurrentSetExample {
public static void main(String[] args) {
Set s = ConcurrentHashMap.newKeySet();
s.add("a");
// безопасно использовать из нескольких потоков
System.out.println(s.contains("a"));
}
}
true
3) Операции множеств через Stream: объединение, пересечение, разность
import java.util.*;
import java.util.stream.*;
class SetOps {
public static void main(String[] args) {
Set a = new HashSet<>(Arrays.asList(1,2,3));
Set b = new HashSet<>(Arrays.asList(2,3,4));
Set union = Stream.concat(a.stream(), b.stream()).collect(Collectors.toCollection(HashSet::new));
Set intersect = a.stream().filter(b::contains).collect(Collectors.toSet());
Set diff = a.stream().filter(x -> !b.contains(x)).collect(Collectors.toSet());
System.out.println(union); // [1, 2, 3, 4]
System.out.println(intersect); // [2, 3]
System.out.println(diff); // [1]
}
}
[1, 2, 3, 4] [2, 3] [1]
4) Сохранять порядок вставки при сборе через Streams
import java.util.*;
import java.util.stream.*;
class CollectorExample {
public static void main(String[] args) {
List list = Arrays.asList("b","a","b","c");
Set s = list.stream()
.collect(Collectors.toCollection(LinkedHashSet::new));
System.out.println(s); // [b, a, c]
}
}
[b, a, c]
5) Переопределение equals/hashCode для пользовательских объектов
import java.util.*;
class User {
final String id;
User(String id){this.id=id;}
public boolean equals(Object o){
return o instanceof User && ((User)o).id.equals(id);
}
public int hashCode(){ return id.hashCode(); }
}
class UserSet {
public static void main(String[] args){
Set s = new HashSet<>();
s.add(new User("1"));
s.add(new User("1"));
System.out.println(s.size()); // 1
}
}
1
6) Использование Guava ImmutableSet и Multiset
// пример с Guava (если библиотека присутствует)
import com.google.common.collect.ImmutableSet;
class GuavaExample {
public static void main(String[] args) {
ImmutableSet s = ImmutableSet.of("a", "b");
System.out.println(s);
}
}
[a, b]
7) Необычная задача: многоключевой поиск через Set комбинаций
import java.util.*;
class PairsExample {
public static void main(String[] args){
Set s = new HashSet<>();
for (int i=0;i<3;i++) for (int j=0;j<3;j++) s.add(i+":"+j);
System.out.println(s.contains("1:2"));
}
}
true
Во всех примерах учитываются контракт equals/hashCode, особенности реализации и потокобезопасность при выборе класса множества.