Un guide sur HashSet en Java

Un guide pour HashSet en Java

1. Vue d'ensemble

Dans cet article, nous allons plonger dansHashSet. C'est l'une des implémentations deSet les plus populaires et une partie intégrante du Java Collections Framework.

2. Introduction àHashSet

HashSet est l'une des structures de données fondamentales de l'API Java Collections.

Rappelons les aspects les plus importants de cette implémentation:

  • Il stocke des éléments uniques et autorise les valeurs nulles

  • Il est soutenu par unHashMap

  • Il ne conserve pas l'ordre d'insertion

  • Ce n’est pas compatible avec les threads

Notez que ceHashMap interne est initialisé lorsqu'une instance duHashSet est créée:

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

Si vous voulez approfondir le fonctionnement deHashMap, vous pouvez lirethe article focused on it here.

3. L'API

Dans cette section, nous allons passer en revue les méthodes les plus couramment utilisées et examiner quelques exemples simples.

3.1. add()

La méthodeadd() peut être utilisée pour ajouter des éléments à un ensemble. The method contract states that an element will be added only when it isn’t already present in a set. Si un élément a été ajouté, la méthode renvoietrue, sinon -false.

Nous pouvons ajouter un élément à unHashSet comme:

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

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

Du point de vue de l'implémentation, la méthodeadd est extrêmement importante. Les détails d'implémentation illustrent le fonctionnement deHashSet en interne et exploitent la méthodeHashMap’sput:

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

La variablemap est une référence au backing interneHashMap:

private transient HashMap map;

Ce serait une bonne idée de se familiariser d'abord avec leshashcode pour comprendre en détail comment les éléments sont organisés dans des structures de données basées sur le hachage.

Résumant:

  • UnHashMap est un tableau debuckets avec une capacité par défaut de 16 éléments - chaque compartiment correspond à une valeur de hashcode différente

  • Si différents objets ont la même valeur hashcode, ils sont stockés dans un seul compartiment.

  • Si leload factor est atteint, un nouveau tableau est créé deux fois la taille du précédent et tous les éléments sont remaniés et redistribués entre les nouveaux buckets correspondants

  • Pour récupérer une valeur, nous hachons une clé, la modifions, puis allons dans un bucket correspondant et recherchons dans la liste liée potentielle au cas où il y aurait plus d'un objet

3.2. contains()

The purpose of the contains method is to check if an element is present in a given HashSet. Il renvoietrue si l'élément est trouvé, sinonfalse.

Nous pouvons rechercher un élément dans lesHashSet:

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

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

Chaque fois qu'un objet est passé à cette méthode, la valeur de hachage est calculée. Ensuite, l'emplacement du compartiment correspondant est résolu et traversé.

3.3. remove()

La méthode supprime l'élément spécifié de l'ensemble s'il est présent. Cette méthode retournetrue si un ensemble contient l'élément spécifié.

Voyons un exemple fonctionnel:

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

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

3.4. clear()

Nous utilisons cette méthode lorsque nous avons l'intention de supprimer tous les éléments d'un ensemble. L'implémentation sous-jacente efface simplement tous les éléments desHashMap. sous-jacents

Voyons cela en action:

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

    assertTrue(clearHashSet.isEmpty());
}

3.5. size()

C'est l'une des méthodes fondamentales de l'API. Il est très utilisé car il aide à identifier le nombre d'éléments présents dans lesHashSet. L'implémentation sous-jacente délègue simplement le calcul à la méthodeHashMap’s size().

Voyons cela en action:

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

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

3.6. isEmpty()

Nous pouvons utiliser cette méthode pour déterminer si une instance donnée d'unHashSet est vide ou non. Cette méthode renvoietrue si l'ensemble ne contient aucun élément:

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

    assertTrue(emptyHashSet.isEmpty());
}

3.7. iterator()

La méthode retourne un itérateur sur les éléments dans lesSet. The elements are visited in no particular order and iterators are fail-fast.

Nous pouvons observer l'ordre d'itération aléatoire ici:

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

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 de suppression de l’itérateur, leIterator lance unConcurrentModificationException.

Voyons cela en action:

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

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

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

Le comportement de défaillance rapide d’un itérateur ne peut pas être garanti car il est impossible de faire des garanties fermes en présence de modifications simultanées non synchronisées.

Les itérateurs à échec rapide lancentConcurrentModificationException sur la base du meilleur effort. Par conséquent, il serait erroné d’écrire un programme dont l’exactitude dépendait de cette exception.

4. CommentHashSet maintient-il l'unicité?

Lorsque nous mettons un objet dans unHashSet, il utilise la valeurhashcode de l’objet pour déterminer si un élément n’est pas déjà dans l’ensemble.

Chaque valeur de code de hachage correspond à un emplacement de compartiment donné pouvant contenir divers éléments, pour lesquels la valeur de hachage calculée est la même. But two objects with the same hashCode might not be equal.

Ainsi, les objets du même compartiment seront comparés en utilisant la méthodeequals().

5. Performance deHashSet

La performance d'unHashSet est principalement affectée par deux paramètres - sesInitial Capacity et lesLoad Factor.

La complexité temporelle attendue de l'ajout d'un élément à un ensemble est deO(1) qui peut chuter àO(n) dans le pire des cas (un seul compartiment présent) - donc,it’s essential to maintain the right HashSet’s capacity.

Une note importante: depuis JDK 8,the worst case time complexity is O(log*n).

Le facteur de charge décrit le niveau de remplissage maximal au-dessus duquel un ensemble doit être redimensionné.

Nous pouvons également créer unHashSet avec des valeurs personnalisées pourinitial capacity etload factor:

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

Dans le premier cas, les valeurs par défaut sont utilisées - la capacité initiale de 16 et le facteur de charge de 0,75. Dans le second cas, nous remplaçons la capacité par défaut et dans le troisième, nous substituons les deux.

Une faible capacité initiale réduit la complexité de l'espace mais augmente la fréquence de rechargement qui est un processus coûteux.

D'autre part,a high initial capacity increases the cost of iteration and the initial memory consumption.

En règle générale:

  • Une capacité initiale élevée est bonne pour un grand nombre d'entrées couplées avec peu ou pas d'itération

  • Une faible capacité initiale est bonne pour quelques entrées avec beaucoup d'itération

Il est donc très important de trouver le juste équilibre entre les deux. Habituellement, la mise en œuvre par défaut est optimisée et fonctionne parfaitement. Si nous ressentons le besoin d'adapter ces paramètres pour répondre aux exigences, nous devons procéder de manière judicieuse.

6. Conclusion

Dans cet article, nous avons décrit l'utilité d'unHashSet, son objectif ainsi que son fonctionnement sous-jacent. Nous avons constaté son efficacité en termes de convivialité, compte tenu de ses performances constantes dans le temps et de sa capacité à éviter les doublons.

Nous avons étudié certaines des méthodes importantes de l'API, comment elles peuvent nous aider en tant que développeur à utiliser unHashSet à son potentiel.

Comme toujours, des extraits de code peuvent être trouvésover on GitHub.