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

Примеры и приёмы работы с Set
Раздел: Коллекции, Set
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 s = new TreeSet<>();
        s.add(1); s.add("a"); // ClassCastException
    }
}

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 - компактное и быстрое множество для перечислений

Пример java
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

Пример java
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: объединение, пересечение, разность

Пример java
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

Пример java
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 для пользовательских объектов

Пример java
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

Пример java
// пример с 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 комбинаций

Пример java
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, особенности реализации и потокобезопасность при выборе класса множества.

джава Set function comments

En
Set Интерфейс для коллекций без дубликатов