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.