Um guia para o HashSet em Java

Um guia para o HashSet em Java

1. Visão geral

Neste artigo, vamos mergulhar emHashSet.. É uma das implementações deSet mais populares, bem como uma parte integrante do Java Collections Framework.

2. Introdução aHashSet

HashSet é uma das estruturas de dados fundamentais na API de coleções Java.

Vamos relembrar os aspectos mais importantes desta implementação:

  • Armazena elementos únicos e permite valores nulos

  • É apoiado por umHashMap

  • Não mantém o pedido de inserção

  • Não é thread-safe

Observe que esteHashMap interno é inicializado quando uma instância deHashSet é criada:

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

Se você quiser se aprofundar em como oHashMap funciona, você pode lerthe article focused on it here.

3. A API

Nesta seção, vamos revisar os métodos mais comumente usados ​​e dar uma olhada em alguns exemplos simples.

3.1. add()

O métodoadd() pode ser usado para adicionar elementos a um conjunto. The method contract states that an element will be added only when it isn’t already present in a set. Se um elemento foi adicionado, o método retornatrue, caso contrário -false.

Podemos adicionar um elemento a umHashSet como:

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

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

De uma perspectiva de implementação, o métodoadd é extremamente importante. Os detalhes de implementação ilustram como oHashSet funciona internamente e aproveita o métodoHashMap’sput:

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

A variávelmap é uma referência ao interno, apoiandoHashMap:

private transient HashMap map;

Seria uma boa ideia se familiarizar com oshashcode primeiro para obter uma compreensão detalhada de como os elementos são organizados em estruturas de dados baseadas em hash.

Resumindo:

  • UmHashMap é uma matriz debuckets com uma capacidade padrão de 16 elementos - cada intervalo corresponde a um valor de hashcode diferente

  • Se vários objetos tiverem o mesmo valor de código de hash, eles serão armazenados em um único bucket

  • Seload factor for alcançado, uma nova matriz é criada com o dobro do tamanho da anterior e todos os elementos são refeitos e redistribuídos entre os novos baldes correspondentes

  • Para recuperar um valor, fazemos um hash de uma chave, modificamos e, em seguida, vamos para um intervalo correspondente e pesquisamos na lista potencial vinculada no caso de haver mais de um objeto

3.2. contains()

The purpose of the contains method is to check if an element is present in a given HashSet. Retornatrue se o elemento for encontrado, caso contrário,false.

Podemos verificar se há um elemento emHashSet:

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

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

Sempre que um objeto é passado para esse método, o valor do hash é calculado. Em seguida, o local do depósito correspondente é resolvido e percorrido.

3.3. remove()

O método remove o elemento especificado do conjunto, se estiver presente. Este método retornatrue se um conjunto contiver o elemento especificado.

Vejamos um exemplo prático:

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

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

3.4. clear()

Usamos esse método quando pretendemos remover todos os itens de um conjunto. A implementação subjacente simplesmente limpa todos os elementos dosHashMap. subjacentes

Vamos ver isso em ação:

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

    assertTrue(clearHashSet.isEmpty());
}

3.5. size()

Este é um dos métodos fundamentais na API. É muito usado, pois ajuda a identificar o número de elementos presentes emHashSet. A implementação subjacente simplesmente delega o cálculo ao métodoHashMap’s size().

Vamos ver isso em ação:

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

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

3.6. isEmpty()

Podemos usar este método para descobrir se uma determinada instância deHashSet está vazia ou não. Este método retornatrue se o conjunto não contiver elementos:

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

    assertTrue(emptyHashSet.isEmpty());
}

3.7. iterator()

O método retorna um iterador sobre os elementos emSet. The elements are visited in no particular order and iterators are fail-fast.

Podemos observar a ordem da iteração aleatória aqui:

@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());
    }
}

Se o conjunto for modificado a qualquer momento após o iterador ser criado de qualquer maneira, exceto através do próprio método de remoção do iterador, oIterator lança umConcurrentModificationException

Vamos ver isso em ação:

@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");
    }
}

Alternativamente, se tivéssemos usado o método remove do iterador, não teríamos encontrado a exceção:

@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());
}

O comportamento rápido de falha de um iterador não pode ser garantido, pois é impossível fazer quaisquer garantias rígidas na presença de modificação simultânea não sincronizada.

Os iteradores fail-fast lançamConcurrentModificationException com base no melhor esforço. Portanto, seria errado escrever um programa que dependesse dessa exceção para sua correção.

4. ComoHashSet mantém a exclusividade?

Quando colocamos um objeto em umHashSet, ele usa o valorhashcode do objeto para determinar se um elemento ainda não está no conjunto.

Cada valor do código de hash corresponde a um determinado local do depósito que pode conter vários elementos, para os quais o valor calculado do hash é o mesmo. But two objects with the same hashCode might not be equal.

Portanto, os objetos dentro do mesmo intervalo serão comparados usando o métodoequals().

5. Desempenho deHashSet

O desempenho de aHashSet é afetado principalmente por dois parâmetros - seuInitial CapacityeLoad Factor.

A complexidade de tempo esperada para adicionar um elemento a um conjunto éO(1), que pode cair paraO(n) no pior cenário (apenas um intervalo presente) - portanto,it’s essential to maintain the right HashSet’s capacity.

Uma nota importante: desde JDK 8,the worst case time complexity is O(log*n).

O fator de carga descreve qual é o nível máximo de preenchimento, acima do qual, um conjunto precisará ser redimensionado.

Também podemos criar umHashSet com valores personalizados parainitial capacityeload factor:

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

No primeiro caso, os valores padrão são usados ​​- a capacidade inicial de 16 e o ​​fator de carga de 0,75. No segundo, substituímos a capacidade padrão e no terceiro, substituímos ambos.

Uma baixa capacidade inicial reduz a complexidade do espaço, mas aumenta a frequência de rehashing, que é um processo caro.

Por outro lado,a high initial capacity increases the cost of iteration and the initial memory consumption.

Como um princípio básico:

  • Uma alta capacidade inicial é boa para um grande número de entradas, juntamente com pouca ou nenhuma iteração

  • Uma capacidade inicial baixa é boa para poucas entradas com muita iteração

É, portanto, muito importante encontrar o equilíbrio correto entre os dois. Geralmente, a implementação padrão é otimizada e funciona muito bem, se sentirmos a necessidade de ajustar esses parâmetros para atender aos requisitos, precisamos fazer isso criteriosamente.

6. Conclusão

Neste artigo, descrevemos a utilidade de aHashSet, sua finalidade, bem como seu funcionamento subjacente. Vimos como é eficiente em termos de usabilidade, devido ao desempenho constante do tempo e à capacidade de evitar duplicatas.

Estudamos alguns dos métodos importantes da API, como eles podem nos ajudar, como desenvolvedores, a usar umHashSet em seu potencial.

Como sempre, trechos de código podem ser encontradosover on GitHub.