Руководство по HashSet в Java

Руководство по HashSet в Java

1. обзор

В этой статье мы погрузимся вHashSet.. Это одна из самых популярных реализацийSet, а также неотъемлемая часть Java Collections Framework.

2. Введение вHashSet

HashSet - одна из фундаментальных структур данных в Java Collections API.

Напомним наиболее важные аспекты этой реализации:

  • Он хранит уникальные элементы и разрешает нули

  • ПоддерживаетсяHashMap

  • Заказ на размещение не поддерживается.

  • Это не потокобезопасный

Обратите внимание, что этот внутреннийHashMap инициализируется при создании экземпляраHashSet:

public HashSet() {
    map = new HashMap<>();
}

Если вы хотите глубже понять, как работаетHashMap, вы можете прочитатьthe article focused on it here.

3. API

В этом разделе мы рассмотрим наиболее часто используемые методы и рассмотрим несколько простых примеров.

3.1. add()с

Методadd() можно использовать для добавления элементов в набор. The method contract states that an element will be added only when it isn’t already present in a set. Если элемент был добавлен, метод возвращаетtrue,, иначе -false.

Мы можем добавить элемент вHashSet, например:

@Test
public void whenAddingElement_shouldAddElement() {
    Set hashset = new HashSet<>();

    assertTrue(hashset.add("String Added"));
}

С точки зрения реализации методadd чрезвычайно важен. Детали реализации иллюстрируют, какHashSet работает внутренне и использует методHashMap’sput:

public boolean add(E e) {
    return map.put(e, PRESENT) == null;
}

Переменнаяmap является ссылкой на внутреннюю поддержкуHashMap:

private transient HashMap map;

Было бы неплохо сначала познакомиться сhashcode, чтобы получить подробное представление о том, как элементы организованы в структуры данных на основе хешей.

Подводя итог:

  • HashMap - это массивbuckets с емкостью по умолчанию 16 элементов - каждому сегменту соответствует другое значение хэш-кода.

  • Если различные объекты имеют одинаковое значение хэш-кода, они сохраняются в одном сегменте

  • Если достигаетсяload factor, создается новый массив, вдвое превышающий размер предыдущего, и все элементы повторно хешируются и перераспределяются между новыми соответствующими сегментами.

  • Чтобы получить значение, мы хэшируем ключ, модифицируем его, а затем переходим к соответствующему сегменту и ищем в потенциально связанном списке, если существует более одного объекта.

3.2. contains()с

The purpose of the contains method is to check if an element is present in a given HashSet. Возвращаетtrue, если элемент найден, иначеfalse.

Мы можем проверить наличие элемента вHashSet:

@Test
public void whenCheckingForElement_shouldSearchForElement() {
    Set hashsetContains = new HashSet<>();
    hashsetContains.add("String Added");

    assertTrue(hashsetContains.contains("String Added"));
}

Всякий раз, когда объект передается этому методу, вычисляется значение хеша. Затем соответствующее местоположение сегмента решается и пересекается.

3.3. remove()с

Метод удаляет указанный элемент из набора, если он присутствует. Этот метод возвращаетtrue, если набор содержал указанный элемент.

Давайте посмотрим на рабочий пример:

@Test
public void whenRemovingElement_shouldRemoveElement() {
    Set removeFromHashSet = new HashSet<>();
    removeFromHashSet.add("String Added");

    assertTrue(removeFromHashSet.remove("String Added"));
}

3.4. clear()с

Мы используем этот метод, когда намереваемся удалить все элементы из набора. Базовая реализация просто очищает все элементы из базовогоHashMap.

Посмотрим, как это работает:

@Test
public void whenClearingHashSet_shouldClearHashSet() {
    Set clearHashSet = new HashSet<>();
    clearHashSet.add("String Added");
    clearHashSet.clear();

    assertTrue(clearHashSet.isEmpty());
}

3.5. size()с

Это один из фундаментальных методов в API. Он широко используется, поскольку помогает определить количество элементов, присутствующих вHashSet. Базовая реализация просто делегирует вычисление методуHashMap’s size().

Посмотрим, как это работает:

@Test
public void whenCheckingTheSizeOfHashSet_shouldReturnThesize() {
    Set hashSetSize = new HashSet<>();
    hashSetSize.add("String Added");

    assertEquals(1, hashSetSize.size());
}

3.6. isEmpty()с

Мы можем использовать этот метод, чтобы выяснить, является ли данный экземплярHashSet пустым или нет. Этот метод возвращаетtrue, если набор не содержит элементов:

@Test
public void whenCheckingForEmptyHashSet_shouldCheckForEmpty() {
    Set emptyHashSet = new HashSet<>();

    assertTrue(emptyHashSet.isEmpty());
}

3.7. iterator()с

Метод возвращает итератор по элементам вSet. The elements are visited in no particular order and iterators are fail-fast.

Мы можем наблюдать случайный порядок итераций здесь:

@Test
public void whenIteratingHashSet_shouldIterateHashSet() {
    Set hashset = new HashSet<>();
    hashset.add("First");
    hashset.add("Second");
    hashset.add("Third");
    Iterator itr = hashset.iterator();
    while(itr.hasNext()){
        System.out.println(itr.next());
    }
}

Если набор изменяется в любое время после создания итератора любым способом, кроме собственного метода удаления итератора,Iterator генерируетConcurrentModificationException.

Посмотрим, как это работает:

@Test(expected = ConcurrentModificationException.class)
public void whenModifyingHashSetWhileIterating_shouldThrowException() {

    Set hashset = new HashSet<>();
    hashset.add("First");
    hashset.add("Second");
    hashset.add("Third");
    Iterator itr = hashset.iterator();
    while (itr.hasNext()) {
        itr.next();
        hashset.remove("Second");
    }
}

В качестве альтернативы, если бы мы использовали метод удаления итератора, мы бы не столкнулись с исключением:

@Test
public void whenRemovingElementUsingIterator_shouldRemoveElement() {

    Set hashset = new HashSet<>();
    hashset.add("First");
    hashset.add("Second");
    hashset.add("Third");
    Iterator itr = hashset.iterator();
    while (itr.hasNext()) {
        String element = itr.next();
        if (element.equals("Second"))
            itr.remove();
    }

    assertEquals(2, hashset.size());
}

Безотказное поведение итератора не может быть гарантировано, поскольку невозможно дать никаких жестких гарантий при наличии несинхронизированной одновременной модификации.

Отказоустойчивые итераторы выбрасываютConcurrentModificationException из соображений максимальной эффективности. Поэтому было бы неправильно писать программу, правильность которой зависела бы от этого исключения.

4. КакHashSet сохраняет уникальность?

Когда мы помещаем объект вHashSet, он использует значение объектаhashcode, чтобы определить, есть ли элемент уже в наборе.

Каждое значение хеш-кода соответствует определенному местоположению сегмента, которое может содержать различные элементы, для которых вычисленное значение хеш-кода является одинаковым. But two objects with the same hashCode might not be equal.

Таким образом, объекты в одной корзине будут сравниваться с использованием методаequals().

5. ПроизводительностьHashSet

На производительность aHashSet в основном влияют два параметра - егоInitial Capacity иLoad Factor.

Ожидаемая временная сложность добавления элемента в набор составляетO(1), которая может упасть доO(n) в худшем случае (присутствует только одна корзина) - следовательно,it’s essential to maintain the right HashSet’s capacity.

Важное примечание: начиная с JDK 8,the worst case time complexity is O(log*n).

Коэффициент загрузки описывает максимальный уровень заполнения, выше которого необходимо изменить размер набора.

Мы также можем создатьHashSet с пользовательскими значениями дляinitial capacity иload factor:

Set hashset = new HashSet<>();
Set hashset = new HashSet<>(20);
Set hashset = new HashSet<>(20, 0.5f);

В первом случае используются значения по умолчанию - начальная емкость 16 и коэффициент загрузки 0,75. Во втором мы переопределяем емкость по умолчанию, а в третьем переопределяем оба.

Низкая начальная емкость снижает сложность пространства, но увеличивает частоту повторного хеширования, что является дорогостоящим процессом.

С другой стороны,a high initial capacity increases the cost of iteration and the initial memory consumption.

Как правило большого пальца:

  • Высокая начальная емкость хороша для большого количества записей в сочетании с минимальной итерацией

  • Низкая начальная емкость хороша для нескольких записей с большим количеством итераций

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

6. Заключение

В этой статье мы описали полезностьHashSet, его назначение, а также основную работу. Мы увидели, насколько он эффективен с точки зрения удобства использования, учитывая его постоянную производительность по времени и возможность избежать дублирования.

Мы изучили некоторые важные методы из API, как они могут помочь нам как разработчику использоватьHashSet в полной мере.

Как всегда, фрагменты кода можно найтиover on GitHub.