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

Примеры и руководство по LinkedHashSet
Раздел: Коллекции, Set
LinkedHashSet: class

Общее описание LinkedHashSet

Класс LinkedHashSet из пакета java.util представляет реализацию множества (Set), которая сохраняет порядок вставки элементов. В основе лежит LinkedHashMap, что обеспечивает уникальность элементов по контракту equals/hashCode и воспроизведение порядка добавления при итерации.

Основные случаи применения: хранение уникальных элементов при необходимости воспроизвести их порядок вставки, удаление дубликатов с сохранением последовательности, преобразование потоков в упорядоченное множество.

Конструкторы и их параметры:

  • LinkedHashSet() - создаёт пустое множество с начальными настройками внутренней карты.
  • LinkedHashSet(int initialCapacity) - задаёт начальную вместимость внутренней карты. Полезно для оптимизации при заранее известном числе элементов.
  • LinkedHashSet(int initialCapacity, float loadFactor) - задаёт вместимость и коэффициент заполнения (load factor) для внутренней карты. Коэффициент контролирует момент перераспределения (rehash).
  • LinkedHashSet(Collection c) - создаёт множество и добавляет в него все элементы заданной коллекции в порядке, который будет соответствовать порядку итерации этой коллекции при добавлении.

Ключевые методы и возвращаемые значения:

  • boolean add(E e) - добавляет элемент; возвращает true, если элемент отсутствовал и был добавлен, false, если элемент уже присутствовал.
  • boolean remove(Object o) - удаляет объект; возвращает true, если элемент был найден и удалён.
  • boolean contains(Object o) - проверяет наличие; возвращает true, если элемент присутствует.
  • Iterator iterator() - возвращает итератор, проходящий элементы в порядке их вставки.
  • int size(), boolean isEmpty(), void clear() - стандартные операции над коллекцией.
  • Object clone() - создаёт поверхностную (shallow) копию множества; элементы не клонируются.
  • Object[] toArray() и <T> T[] toArray(T[] a) - преобразование в массив.
  • Методы bulk-операций: addAll, removeAll, retainAll, containsAll - возвращают стандартные логические значения согласно контракту Collection.

Особенности поведения:

  • Порядок итерации соответствует порядку вставки элементов (вставленный раньше элемент будет возвращён раньше при итерации), при повторной вставке уже существующего элемента порядок не меняется. Если элемент удалён и затем добавлен снова, он окажется в конце порядковой последовательности.
  • Допускается значение null (один элемент null), реализация хранит ключи в карте и может обрабатывать null как обычный элемент.
  • Класс не потокобезопасен; при совместном доступе из нескольких потоков требуется внешняя синхронизация или использование потокобезопасных обёрток.
  • Производительность: операции добавления, удаления и проверки присутствия - амортизированно O(1). Накладные расходы выше, чем у простого HashSet, из-за связанного списка для порядка.

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

Пример 1. Простое добавление и итерация в порядке вставки.

import java.util.LinkedHashSet;

public class Example1 {
    public static void main(String[] args) {
        LinkedHashSet<String> set = new LinkedHashSet<>();
        set.add("one");
        set.add("two");
        set.add("three");
        set.add("two"); // дубликат игнорируется
        for (String s : set) {
            System.out.println(s);
        }
    }
}
one
two
three

Пример 2. Конструктор с коллекцией и проверка возвращаемых значений методов.

import java.util.Arrays;
import java.util.LinkedHashSet;

public class Example2 {
    public static void main(String[] args) {
        LinkedHashSet<Integer> numbers = new LinkedHashSet<>(Arrays.asList(3, 1, 4, 1, 5));
        System.out.println(numbers.contains(1));      // true
        System.out.println(numbers.add(4));            // false, 4 уже есть
        System.out.println(numbers.add(9));            // true
        System.out.println(numbers.size());            // 5
        System.out.println(numbers);                   // порядок вставки
    }
}
true
false
true
5
[3, 1, 4, 5, 9]

Пример 3. Удаление во время итерации с использованием итератора (корректный способ).

import java.util.Iterator;
import java.util.LinkedHashSet;

public class Example3 {
    public static void main(String[] args) {
        LinkedHashSet<String> set = new LinkedHashSet<>();
        set.add("a"); set.add("b"); set.add("c");
        Iterator<String> it = set.iterator();
        while (it.hasNext()) {
            String s = it.next();
            if ("b".equals(s)) {
                it.remove();
            }
        }
        System.out.println(set);
    }
}
[a, c]

Аналоги в Java и их особенности

  • HashSet - не гарантирует порядок итерации, чуть более быстрый и менее затратный по памяти. Предпочтение при отсутствии требования к порядку.
  • TreeSet - упорядочивает элементы по естественному порядку или по заданному Comparator; операции логарифмической сложности. Выбор при необходимости сортировки.
  • EnumSet - специализированная и очень эффективная реализация для элементов-перечислений; не сохраняет порядок вставки, но предоставляет компактное внутреннее представление.
  • CopyOnWriteArraySet - потокобезопасная реализация на базе копирования при записи; пригодна для небольшого числа записей и множества чтений.
  • LinkedHashMap - не Set, но может использоваться для аналогичных задач с дополнительными возможностями (например, режим доступа для реализации LRU). LinkedHashSet основан на LinkedHashMap.

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

JavaScript - Set в ECMAScript сохраняет порядок вставки. Операции похожи, но отсутствует прямой контроль коэффициента заполнения или начальной вместимости.

const s = new Set(['a','b','a']);
console.log([...s]);
[ 'a', 'b' ]

Python - встроенный set не гарантирует определённый порядок (реализация непредсказуема), для сохранения порядка часто применяются dict.fromkeys() или коллекции из сторонних библиотек. Начиная с CPython 3.7 гарантии касаются dict, но set остаётся неупорядоченным.

# сохранение порядка и удаление дубликатов
lst = [3,1,4,1,5]
unique_ordered = list(dict.fromkeys(lst))
print(unique_ordered)
[3, 1, 4, 5]

PHP - нет встроенного Set; часто используются ассоциативные массивы с ключами-значениями для обеспечения уникальности и порядка:

$arr = ['a','b','a'];
$uniq = array_keys(array_flip($arr));
print_r($uniq);
Array
(
    [0] => a
    [1] => b
)

C# - HashSet<T> не гарантирует порядок. Для сохранения порядка можно комбинировать List<T> и HashSet<T> или использовать сторонние реализации упорядоченного множества.

// C# пример: сохранить порядок вручную
var set = new HashSet<int>();
var list = new List<int>();
int[] input = {1,2,1,3};
foreach (var x in input) {
    if (set.Add(x)) list.Add(x);
}
Console.WriteLine(string.Join(",", list));
1,2,3

Kotlin - есть linkedSetOf() и LinkedHashSet с поведением, совпадающим с Java-реализацией.

val set = linkedSetOf(1,2,1,3)
println(set) // [1, 2, 3]
[1, 2, 3]

Go - нет встроенного типа Set; часто используется map[T]struct{}. Для сохранения порядка применяется дополнительный срез (slice) для индексации последовательности.

// упрощённый пример
input := []int{1,2,1,3}
seen := make(map[int]struct{})
var order []int
for _, v := range input {
    if _, ok := seen[v]; !ok {
        seen[v] = struct{}{}
        order = append(order, v)
    }
}
fmt.Println(order)
[1 2 3]

Lua - таблицы используются как множества, порядок не гарантируется для ключей; если порядок критичен, применяется список ключей отдельно.

В целом отличия: в Java LinkedHashSet обеспечивает простое сохранение порядка и стандартные коллекционные интерфейсы; во многих языках для аналогичной функциональности требуется комбинирование структур или использование специализированных коллекций.

Типичные ошибки и ловушки

  • ConcurrentModificationException: попытка модификации множества напрямую во время итерации (кроме Iterator.remove()) приводит к исключению.
import java.util.LinkedHashSet;

public class FailMod {
    public static void main(String[] args) {
        LinkedHashSet<String> s = new LinkedHashSet<>();
        s.add("a"); s.add("b");
        for (String x : s) {
            s.remove("a"); // ConcurrentModificationException
        }
    }
}
Exception in thread "main" java.util.ConcurrentModificationException
    at java.base/java.util.LinkedHashMap$LinkedKeyIterator.next(LinkedHashMap.java:...)
    ...
  • Изменяемые ключи: хранение объектов, у которых меняются поля, участвующие в equals/hashCode, нарушает корректность множества: элемент может стать «невидимым» для поиска.
class Mutable {
    int id;
    Mutable(int id){this.id=id;}
    public int hashCode(){return id;}
    public boolean equals(Object o){return o instanceof Mutable && ((Mutable)o).id==id;}
}
// затем в множестве элемент изменяет id и поиск по старому значению не удаётся
Непредсказуемое поведение: contains() вернёт false для изменённого объекта, хотя он присутствует в структуре.
  • Ожидание сортировки: LinkedHashSet не сортирует элементы; для сортировки применяется TreeSet или сортировка отдельной коллекции.

Изменения в реализации и заметные правки

Архитектура LinkedHashSet основана на LinkedHashMap и в современных версиях JDK не претерпела существенных изменений в публичном API. Непосредственно в LinkedHashSet не вводилось новых конструкторов или флагов в последних релизах. Улучшения производительности и оптимизации затрагивали внутренние карты и коллекции в целом, но контракт класса остался прежним. В новых версиях языка появились более мощные возможности работы с потоками (Stream API) и фабрики коллекций, которые упрощают создание неизменяемых или компактных наборов, но они не заменяют семантику LinkedHashSet.

Расширенные и редкие сценарии использования

Пример A. Реализация простого ограниченного буфера (FIFO) с сохранением уникальности - если размер превышен, удаляется самый старый элемент.

Пример java
import java.util.Iterator;
import java.util.LinkedHashSet;

public class BoundedSet {
    private final int limit;
    private final LinkedHashSet<E> set;

    public BoundedSet(int limit) {
        this.limit = limit;
        this.set = new LinkedHashSet<>();
    }

    public boolean add(E e) {
        if (set.contains(e)) return false;
        set.add(e);
        if (set.size() > limit) {
            Iterator<E> it = set.iterator();
            if (it.hasNext()) it.next();
            it.remove();
        }
        return true;
    }

    public String toString(){ return set.toString(); }

    public static void main(String[] args) {
        BoundedSet<Integer> buf = new BoundedSet<>(3);
        buf.add(1); buf.add(2); buf.add(3);
        buf.add(4); // удалится 1
        buf.add(2); // 2 уже есть, порядок не меняется
        System.out.println(buf);
    }
}
[2, 3, 4]

Пояснение: LinkedHashSet применяется как упорядоченный контейнер уникальных элементов; удаление самого старого происходит через итератор.

Пример B. Сохранение порядка при объединении коллекций и последующая фильтрация с использованием Stream API.

Пример java
import java.util.*;
import java.util.stream.Collectors;

public class MergeKeepOrder {
    public static void main(String[] args) {
        List<String> a = Arrays.asList("x","y","z");
        List<String> b = Arrays.asList("y","w");
        LinkedHashSet<String> merged = new LinkedHashSet<>();
        merged.addAll(a);
        merged.addAll(b);
        // фильтрация по длине строки с сохранением порядка
        List<String> result = merged.stream()
            .filter(s -> s.length() == 1)
            .collect(Collectors.toList());
        System.out.println(result);
    }
}
[x, y, z, w]

Пример C. Резервирование начальной ёмкости для больших множеств, чтобы уменьшить количество реhash-операций.

Пример java
LinkedHashSet<Integer> big = new LinkedHashSet<>(1000, 0.75f);
for (int i=0; i<1000; i++) big.add(i);
System.out.println(big.size());
1000

Пример D. Создание потокобезопасного множества с сохранением порядка через обёртку.

Пример java
import java.util.Collections;
import java.util.Set;
import java.util.LinkedHashSet;

Set<String> sync = Collections.synchronizedSet(new LinkedHashSet<>());
// при итерации требуется синхронизация вручную
synchronized(sync) {
    for (String s : sync) {
        // обход
    }
}
Нет конкретного вывода; стиль демонстрирует требуемую синхронизацию при итерации.

Пример E. Клонирование и сохранение порядка (shallow copy).

Пример java
LinkedHashSet<String> orig = new LinkedHashSet<>();
orig.add("a"); orig.add("b");
@SuppressWarnings("unchecked")
LinkedHashSet<String> copy = (LinkedHashSet<String>) orig.clone();
System.out.println(copy);
[a, b]

Пример F. Использование в качестве промежуточной структуры для удаления дубликатов при сохранении порядка элементов входного массива.

Пример java
String[] input = {"apple","banana","apple","pear"};
LinkedHashSet<String> unique = new LinkedHashSet<>(Arrays.asList(input));
String[] out = unique.toArray(new String[0]);
System.out.println(Arrays.toString(out));
[apple, banana, pear]

Пример G. Комбинация с WeakHashMap для «слабого» множества (не входит в стандартную библиотеку; схема для примера).

Пример java
// В стандартной библиотеке нет WeakHashSet, но можно реализовать обёртку
// на базе WeakHashMap<E, Boolean> и вспомогательного списка для порядка.
// Такой код требует аккуратной реализации и не является тривиальным примером.
Нет вывода; иллюстрация концепции и предостережение о дополнительной сложности.

джава LinkedHashSet function comments

En
LinkedHashSet HashSet с сохранением порядка добавления