Un guide sur TreeSet en Java

1. Vue d’ensemble

Dans cet article, nous examinerons une partie intégrante de Java Collections Framework et l’une des implémentations les plus courantes de Set - le TreeSet .

2. Introduction à TreeSet

En bref, TreeSet est une collection triée qui étend la classe AbstractSet et implémente l’interface NavigableSet .

Voici un bref résumé des aspects les plus importants de cette implémentation:

  • Il stocke des éléments uniques

  • Il ne conserve pas l’ordre d’insertion des éléments

  • Il trie les éléments dans l’ordre croissant

  • Ce n’est pas thread-safe

  • Dans cette implémentation, les objets sont triés et stockés dans un ordre croissant en fonction de leur ordre naturel ** . Le TreeSet utilise un arbre de recherche binaire à auto-équilibrage, plus spécifiquement https://en.wikipedia.org/wiki/Red%E2%80%93black tree[a Red-Black__ tree].

En termes simples, étant un arbre de recherche binaire à auto-équilibrage, chaque nœud de l’arbre binaire comprend un bit supplémentaire, qui est utilisé pour identifier la couleur du nœud qui est rouge ou noir. Lors des insertions et suppressions ultérieures, ces bits de «couleur» aident à garantir que l’arbre reste plus ou moins équilibré.

Créons donc une instance de TreeSet :

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

2.1. TreeSet avec un paramètre de comparateur de constructeur

Facultativement, nous pouvons construire un TreeSet avec un constructeur nous permettant de définir l’ordre dans lequel les éléments sont triés en utilisant un Comparable ou Comparator:

Set<String> treeSet = new TreeSet<>(Comparator.comparing(String::length));
  • Bien que TreeSet ne soit pas thread-safe, il peut être synchronisé en externe à l’aide du wrapper Collections.synchronizedSet () : **

Set<String> syncTreeSet = Collections.synchronizedSet(treeSet);

Très bien, maintenant que nous avons une idée claire de la création d’une instance TreeSet , examinons les opérations courantes disponibles.

3. TreeSet add ()

Comme prévu, la méthode add () peut être utilisée pour ajouter des éléments à un TreeSet . Si un élément a été ajouté, la méthode retourne true, sinon - false.

  • Le contrat de la méthode indique qu’un élément ne sera ajouté que si celui-ci n’est pas déjà présent dans le Set . **

Ajoutons un élément à un TreeSet :

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

    assertTrue(treeSet.add("String Added"));
 }
  • La méthode add est extrêmement importante car ses détails d’implémentation illustrent le fonctionnement interne de TreeSet ** , comment elle exploite la méthode TreeMap’s put pour stocker les éléments:

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

La variable m fait référence à un support interne TreeMap (notez que TreeMap implémente NavigateableMap ):

private transient NavigableMap<E, Object> m;

Par conséquent, TreeSet dépend en interne d’un support NavigableMap qui est initialisé avec une instance de TreeMap lorsqu’une instance de TreeSet est créée:

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

Vous trouverez plus d’informations à ce sujet dans le lien:/java-treemap[cet article].

4. TreeSet contient ()

  • La méthode contains () est utilisée pour vérifier si un élément donné est présent dans un TreeSet donné. ** Si l’élément est trouvé, il renvoie true, sinon false.

Voyons le contains () en action:

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

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

5. TreeSet remove ()

  • La méthode remove () est utilisée pour supprimer l’élément spécifié de l’ensemble si celui-ci est présent. **

Si un ensemble contenait l’élément spécifié, cette méthode retourne true.

Voyons le en action:

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

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

6. TreeSet clear ()

Si nous voulons supprimer tous les éléments d’un ensemble, nous pouvons utiliser la méthode clear () :

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

    assertTrue(clearTreeSet.isEmpty());
}

7. TreeSet size ()

La méthode size () est utilisée pour identifier le nombre d’éléments présents dans le TreeSet . C’est l’une des méthodes fondamentales de l’API:

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

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

8. TreeSet isEmpty ()

La méthode isEmpty () peut être utilisée pour déterminer si une instance donnée de TreeSet est vide ou non:

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

    assertTrue(emptyTreeSet.isEmpty());
}

9. TreeSet iterator ()

La méthode iterator () renvoie un itérateur itérant dans l’ordre croissant les éléments du Set. . Ces itérateurs sont fast-fast .

Nous pouvons observer l’ordre des itérations ascendantes ici:

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

De plus, TreeSet nous permet de parcourir le Set dans l’ordre décroissant.

Voyons cela en action:

@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());
    }
}
  • Le Iterator lève une exception _ConcurrentModificationException i si l’ensemble est modifié à tout moment après la création de l’itérateur de quelque manière que ce soit, sauf par la méthode remove () _ de l’itérateur.

Créons un test pour cela:

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

Sinon, si nous avions utilisé la méthode remove de l’itérateur, nous n’aurions pas rencontré l’exception:

@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());
}
  • Il n’ya aucune garantie sur le comportement à la défaillance d’un itérateur, car il est impossible de donner des garanties fermes en présence de modifications simultanées non synchronisées. **

Pour en savoir plus, consultez le lien:/java-fail-safe-vs-fail-fast-iterator[ici].

10. TreeSet premier ()

Cette méthode retourne le premier élément d’un TreeSet s’il n’est pas vide. Sinon, il lève une NoSuchElementException .

Voyons un exemple:

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

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

11. TreeSet last ()

De manière analogue à l’exemple ci-dessus, cette méthode retournera le dernier élément si l’ensemble n’est pas vide:

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

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

12. TreeSet subSet ()

Cette méthode renverra les éléments allant de fromElement à toElement. Notez que fromElement est inclusif et toElement est exclusif:

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

Cette méthode retournera des éléments de TreeSet qui sont plus petits que l’élément spécifié:

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

Cette méthode renverra les éléments d’un TreeSet qui sont supérieurs ou égaux à l’élément spécifié:

@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. Stockage de Null Elements

  • Avant Java 7, il était possible d’ajouter des éléments nuls à un TreeSet vide. **

Cependant, cela a été considéré comme un bug. Par conséquent, TreeSet ne prend plus en charge l’ajout de null .

Lorsque nous ajoutons des éléments à TreeSet, les éléments sont triés selon leur ordre naturel ou comme spécifié par le comparateur. Par conséquent, l’ajout d’un null, par rapport à des éléments existants, produit une NullPointerException , car null ne peut être comparé à aucune valeur. :

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

Les éléments insérés dans TreeSet doivent implémenter l’interface Comparable ou au moins être acceptés par le comparateur spécifié. Tous ces éléments doivent être comparables entre eux, i.e. E1.compareTo (e2) ou comparator.compare (e1, e2) ne doit pas lancer une ClassCastException . . **

Voyons un exemple:

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. Performance de TreeSet

Comparé à un HashSet , les performances d’un TreeSet sont inférieures. Des opérations telles que add , remove et search prennent O (journal n) temps, tandis que des opérations telles que l’impression d’éléments n dans un ordre trié nécessitent O (n) temps.

Un TreeSet devrait être notre premier choix si nous voulons conserver nos entrées triées car un TreeSet peut être consulté et parcouru en ordre croissant ou décroissant, et les performances des opérations et des vues croissantes seront probablement plus rapides que celles des vues décroissantes.

Le principe de localisation - désigne le phénomène dans lequel les mêmes valeurs, ou emplacements de stockage associés, sont fréquemment consultés, en fonction du modèle d’accès à la mémoire.

Quand on dit localité:

  • Les données similaires sont souvent consultées par une application avec des

la fréquence ** Si deux entrées sont à proximité pour un ordre, un TreeSet les place

proches les uns des autres dans la structure de données, et donc en mémoire

Un TreeSet étant une structure de données avec une plus grande localité, nous pouvons donc conclure conformément au principe de la localité, que nous devrions donner la préférence à un TreeSet si nous manquons de mémoire et si nous voulons accéder à des éléments relativement proches. les uns aux autres en fonction de leur ordre naturel.

Dans le cas où les données doivent être lues à partir du disque dur (qui a une latence plus grande que les données lues dans le cache ou la mémoire), préférez TreeSet car il a une plus grande localité

17. Conclusion

Dans cet article, nous essayons de comprendre comment utiliser l’implémentation standard TreeSet en Java. Nous avons vu son objectif et son efficacité en termes de convivialité étant donné sa capacité à éviter les doublons et à trier les éléments.

Comme toujours, des extraits de code peuvent être trouvés à l’adresse over sur GitHub .