Um guia para TreeSet em Java

Um guia para TreeSet em Java

*1. Visão geral *

Neste artigo, veremos uma parte integrante do Java Collections Framework e* uma das implementações mais populares do Set - o TreeSet *.

*2. Introdução ao TreeSet *

Simplificando, o TreeSet é uma coleção classificada que estende a classe AbstractSet e implementa a interface NavigableSet.

Aqui está um rápido resumo dos aspectos mais importantes desta implementação:

  • Armazena elementos únicos

  • Não preserva a ordem de inserção dos elementos

  • Classifica os elementos em ordem crescente

  • Não é seguro para threads

    *Nesta implementação, os objetos são classificados e armazenados em ordem crescente, de acordo com sua ordem natural* . O _TreeSet_ usa uma árvore de pesquisa binária com auto balanceamento, mais especificamente https://en.wikipedia.org/wiki/Red%E2%80%93black_tree[a árvore _Red-Black_].

Simplificando, sendo uma árvore de pesquisa binária auto-balanceada, cada nó da árvore binária compreende um bit extra, que é usado para identificar a cor do nó que é vermelho ou preto. Durante inserções e exclusões subsequentes, esses bits "coloridos" ajudam a garantir que a árvore permaneça mais ou menos equilibrada.

Então, vamos criar uma instância de um TreeSet:

Set<String> treeSet = new TreeSet<>();

2.1 TreeSet com um parâmetro de comparação do construtor

Opcionalmente, podemos construir um TreeSet com um construtor que nos permite definir a ordem em que os elementos são classificados usando um Comparable ou _Comparator: _

Set<String> treeSet = new TreeSet<>(Comparator.comparing(String::length));
*Embora _TreeSet_ não seja seguro para threads, ele pode ser sincronizado externamente usando o wrapper _Collections.synchronizedSet () _:*
Set<String> syncTreeSet = Collections.synchronizedSet(treeSet);

Tudo bem, agora que temos uma idéia clara de como criar uma instância TreeSet, vamos dar uma olhada nas operações comuns que temos disponíveis.

3. TreeSet _add () _

O método add () _, como esperado, pode ser usado para adicionar elementos a um _TreeSet. Se um elemento foi adicionado, o método retornará true, _ caso contrário - _false.

*O contrato do método afirma que um elemento será adicionado apenas quando o mesmo ainda não estiver presente no _Set _.*

Vamos adicionar um elemento a um TreeSet:

@Test
public void whenAddingElement_shouldAddElement() {
    Set<String> treeSet = new TreeSet<>();

    assertTrue(treeSet.add("String Added"));
 }
*O método _add_ é extremamente importante, pois os detalhes de implementação do método ilustram como o _TreeSet_ funciona internamente* , como ele aproveita o método _TreeMap's_ _put_ para armazenar os elementos:
public boolean add(E e) {
    return m.put(e, PRESENT) == null;
}

A variável m refere-se a um suporte interno TreeMap (observe que TreeMap implementa NavigateableMap):

private transient NavigableMap<E, Object> m;

Portanto, o TreeSet depende internamente de um NavigableMap de backup, que é inicializado com uma instância de TreeMap quando uma instância do TreeSet é criada:

public TreeSet() {
    this(new TreeMap<E,Object>());
}

Mais sobre isso pode ser encontrado no link:/java-treemap [este artigo].

*4. _TreeSet contém () _ *

*O método _contains () _ é usado para verificar se um determinado elemento está presente em um determinado _TreeSet _.* Se o elemento for encontrado, ele retornará true, caso contrário, _false._

Vamos ver o _contains () _ em ação:

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

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

5._ TreeSet remove () _

*O método _remove () _ é usado para remover o elemento especificado do conjunto, se estiver presente.

Se um conjunto contiver o elemento especificado, esse método retornará true.

Vamos vê-lo em ação:

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

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

===* 6. _TreeSet clear () _ *

Se queremos remover todos os itens de um conjunto, podemos usar o método _clear () _:

@Test
public void whenClearingTreeSet_shouldClearTreeSet() {
    Set<String> clearTreeSet = new TreeSet<>();
    clearTreeSet.add("String Added");
    clearTreeSet.clear();

    assertTrue(clearTreeSet.isEmpty());
}

===* 7. _TreeSet size () _ *

O método size () _ é usado para identificar o número de elementos presentes no _TreeSet. É um dos métodos fundamentais na API:

@Test
public void whenCheckingTheSizeOfTreeSet_shouldReturnThesize() {
    Set<String> treeSetSize = new TreeSet<>();
    treeSetSize.add("String Added");

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

===* 8. * _TreeSet isEmpty () _

O método isEmpty () _ pode ser usado para descobrir se uma determinada instância _TreeSet está vazia ou não:

@Test
public void whenCheckingForEmptyTreeSet_shouldCheckForEmpty() {
    Set<String> emptyTreeSet = new TreeSet<>();

    assertTrue(emptyTreeSet.isEmpty());
}

*9. _TreeSet iterator () _ *

O método iterator () _ retorna um iterador iterando na ordem crescente sobre os elementos no _Set.* Esses iteradores são à prova de falhas *.

Podemos observar a ordem crescente da iteração aqui:

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

Além disso, TreeSet permite iterar através do Set em ordem decrescente.

Vamos ver isso em ação:

@Test
public void whenIteratingTreeSet_shouldIterateTreeSetInDescendingOrder() {
    TreeSet<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    treeSet.add("Second");
    treeSet.add("Third");
    Iterator<String> itr = treeSet.descendingIterator();
    while (itr.hasNext()) {
        System.out.println(itr.next());
    }
}
*O _Iterator_ lança uma __ConcurrentModificationException i__se o conjunto for modificado a qualquer momento após a criação do iterador, exceto pelo método _remove () _ do iterador.*

Vamos criar um teste para isso:

@Test(expected = ConcurrentModificationException.class)
public void whenModifyingTreeSetWhileIterating_shouldThrowException() {
    Set<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    treeSet.add("Second");
    treeSet.add("Third");
    Iterator<String> itr = treeSet.iterator();
    while (itr.hasNext()) {
        itr.next();
        treeSet.remove("Second");
    }
}

Como alternativa, se tivéssemos usado o método de remoção do iterador, não teríamos encontrado a exceção:

@Test
public void whenRemovingElementUsingIterator_shouldRemoveElement() {

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

    assertEquals(2, treeSet.size());
}
*Não há garantia no comportamento à prova de falhas de um iterador, pois é impossível fazer garantias concretas na presença de modificação simultânea não sincronizada.*

Mais sobre isso pode ser encontrado no link:/java-fail-safe-vs-fail-fast-iterator [aqui].

*10. _TreeSet first () _ *

Este método retorna o primeiro elemento de um TreeSet se não estiver vazio. Caso contrário, ele lança uma NoSuchElementException.

Vamos ver um exemplo:

@Test
public void whenCheckingFirstElement_shouldReturnFirstElement() {
    TreeSet<String> treeSet = new TreeSet<>();
    treeSet.add("First");

    assertEquals("First", treeSet.first());
}

===* 11. _TreeSet last () _ *

Analogamente ao exemplo acima, este método retornará o último elemento se o conjunto não estiver vazio:

@Test
public void whenCheckingLastElement_shouldReturnLastElement() {
    TreeSet<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    treeSet.add("Last");

    assertEquals("Last", treeSet.last());
}

===* 12. _TreeSet subSet () _ *

Este método retornará os elementos que variam de fromElement a toElement. Observe que fromElement é inclusivo e toElement é exclusivo:

@Test
public void whenUsingSubSet_shouldReturnSubSetElements() {
    SortedSet<Integer> treeSet = new TreeSet<>();
    treeSet.add(1);
    treeSet.add(2);
    treeSet.add(3);
    treeSet.add(4);
    treeSet.add(5);
    treeSet.add(6);

    Set<Integer> expectedSet = new TreeSet<>();
    expectedSet.add(2);
    expectedSet.add(3);
    expectedSet.add(4);
    expectedSet.add(5);

    Set<Integer> subSet = treeSet.subSet(2, 6);

    assertEquals(expectedSet, subSet);
}

===* 13. _TreeSet headSet () _ *

Este método retornará elementos de TreeSet menores do que o elemento especificado:

@Test
public void whenUsingHeadSet_shouldReturnHeadSetElements() {
    SortedSet<Integer> treeSet = new TreeSet<>();
    treeSet.add(1);
    treeSet.add(2);
    treeSet.add(3);
    treeSet.add(4);
    treeSet.add(5);
    treeSet.add(6);

    Set<Integer> subSet = treeSet.headSet(6);

    assertEquals(subSet, treeSet.subSet(1, 6));
}

===* 14. _TreeSet tailSet () _ *

Este método retornará os elementos de um TreeSet maiores ou iguais ao elemento especificado:

@Test
public void whenUsingTailSet_shouldReturnTailSetElements() {
    NavigableSet<Integer> treeSet = new TreeSet<>();
    treeSet.add(1);
    treeSet.add(2);
    treeSet.add(3);
    treeSet.add(4);
    treeSet.add(5);
    treeSet.add(6);

    Set<Integer> subSet = treeSet.tailSet(3);

    assertEquals(subSet, treeSet.subSet(3, true, 6, true));
}

===* 15. Armazenando Null Elements *

*Antes do Java 7, era possível adicionar elementos _null_ a um*  *_TreeSet ._* vazio

No entanto, isso foi considerado um bug. Portanto, TreeSet não oferece mais suporte à adição de null .

Quando adicionamos elementos ao TreeSet, _ os elementos são classificados de acordo com sua ordem natural ou conforme especificado pelo _comparator. Portanto, adicionando um null, _ quando comparado aos elementos existentes, resulta em uma _NullPointerException, já que null não pode ser comparado a nenhum valor :

@Test(expected = NullPointerException.class)
public void whenAddingNullToNonEmptyTreeSet_shouldThrowException() {
    Set<String> treeSet = new TreeSet<>();
    treeSet.add("First");
    treeSet.add(null);
}

Os elementos inseridos no TreeSet devem implementar a interface Comparable ou pelo menos ser aceitos pelo comparador especificado. Todos esses elementos devem ser comparáveis ​​entre si, i.e . _e1.compareTo (e2) _ ou _comparator.compare (e1, e2) _ não deve gerar uma _ClassCastException _.

Vamos ver um exemplo:

class Element {
    private Integer id;

   //Other methods...
}

Comparator<Element> comparator = (ele1, ele2) -> {
    return ele1.getId().compareTo(ele2.getId());
};

@Test
public void whenUsingComparator_shouldSortAndInsertElements() {
    Set<Element> treeSet = new TreeSet<>(comparator);
    Element ele1 = new Element();
    ele1.setId(100);
    Element ele2 = new Element();
    ele2.setId(200);

    treeSet.add(ele1);
    treeSet.add(ele2);

    System.out.println(treeSet);
}

*16. Desempenho de TreeSet *

Quando comparado a um HashSet, o desempenho de um TreeSet fica no lado inferior. Operações como add, remove e search levam O (log n) _ tempo, enquanto operações como a impressão de elementos _n em ordem classificada requerem _O (n) _ tempo.

Um TreeSet deve ser a nossa principal escolha, se queremos manter nossas entradas classificadas como TreeSet, que podem ser acessadas e percorridas em ordem crescente ou decrescente, e é provável que o desempenho das operações e visualizações ascendentes seja mais rápido que o das descendentes.

O Princípio da localidade - é um termo para o fenômeno no qual os mesmos valores ou locais de armazenamento relacionados são acessados ​​com frequência, dependendo do padrão de acesso à memória.

Quando dizemos localidade:

  • Dados semelhantes são frequentemente acessados ​​por um aplicativo com frequência semelhante *Se duas entradas estiverem próximas, recebem uma ordem, um TreeSet as coloca próximas uma da outra na estrutura de dados e, portanto, na memória

Sendo um TreeSet uma estrutura de dados com maior localidade, podemos concluir, de acordo com o Princípio da Localidade, que devemos dar preferência a um TreeSet se estiver com pouca memória e se quiser acessar elementos relativamente próximos uns aos outros de acordo com sua ordem natural.

Caso os dados precisem ser lidos no disco rígido (com maior latência que os dados lidos no cache ou na memória), prefira TreeSet, pois possui maior localidade

===* 17. Conclusão*

Neste artigo, nos concentramos em entender como usar a implementação padrão TreeSet em Java. Vimos seu objetivo e quão eficiente é em relação à usabilidade, devido à sua capacidade de evitar duplicatas e classificar elementos.

Como sempre, snippets de código podem ser encontrados over no GitHub.