HashSet en Java - java.util.HashSet

13 Sep 2019 13 Sep 2019 7432 vues ESSADDOUKI Mostafa 15 min de lecture
Introduction
1 Nouveautés de Java 11 2 Différences entre JDK, JRE et JVM 3 Structure d'un programme Java - Hello World 4 Mots clés et conventions de dénomination en Java 5 Types de données intégrés en Java 6 Les variables en Java 7 Classes enveloppe - Number, Integer, Double ... 8 Lire les entrées clavier en Java
Structures de contrôle
9 Les opérateurs en Java 10 Les structures conditionnelles en Java 11 Les boucles en Java 12 Instructions de contrôle de boucle - break, continue
Chaines de caractères
13 Les chaines en Java - API String 14 Les chaines en Java - StringBuffer et StringBuilder 15 Les expressions régulières en Java
Programmation OO
16 Objets et classes en Java 17 Modificateurs d'accès Java - public, private, protected et package 18 Méthodes et surcharge des méthodes en Java 19 les constructeurs en Java 20 L'héritage en Java 21 Classes abstraites en Java 22 Interfaces et héritage multiple en Java 23 Les classes imbriquées en Java 24 Les singletons en Java 25 Classes et méthodes génériques 26 Interface fonctionnelle et expressions Lambda en Java
Tableaux et collections
27 Les tableaux en Java 28 Classe Arrays - java.util.Arrays 29 Les listes dynamiques - java.util.ArrayList 30 Les listes chaînées en Java - java.util.LinkedList 31 HashSet en Java - java.util.HashSet 32 HashMap en Java - java.util.HashMap
Gestion des fichiers
33 Comprendre les fichiers informatiques 34 Utilisation des classes Path et Files en Java 35 Lecture et écriture dans un fichier en Java 36 Fichiers à accès aléatoire en Java
Gestion d'exceptions
37 Gestion d'exceptions en Java 38 Créez vos propres classes d'exception en Java
Programmation concurrente
39 Introduction à la programmation concurrente en Java - Multi-threads 40 classe java.lang.Thread 41 Synchronisation des threads en Java
Cours Java pour les débutants — Étape 31 sur 41

HashSet — Ensembles basés sur le hachage

  Prérequis

Maîtriser les concepts de collections Java. Comprendre le fonctionnement des tables de hachage. Connaître les méthodes hashCode() et equals(). Avoir des notions sur les listes chaînées et les arbres binaires.

  Objectifs

Comprendre le fonctionnement d'un HashSet et son implémentation basée sur les tables de hachage. Savoir utiliser les opérations d'ajout, suppression, recherche et parcours. Maîtriser les constructeurs et les méthodes spécifiques. Connaître l'optimisation par arbres binaires (Java 8+).

1. Pourquoi utiliser un Set ?

Set

Set est une collection qui ne contient aucun élément en double. C'est une structure idéale lorsque vous avez besoin de vérifier rapidement l'appartenance d'un élément sans vous soucier de l'ordre.

Les listes (ArrayList, LinkedList) vous permettent de contrôler l'ordre des éléments, mais la recherche d'un élément nécessite de parcourir toute la collection (complexité O(n)). Si vous ne vous souciez pas de l'ordre, des structures comme HashSet offrent des recherches beaucoup plus rapides (complexité O(1) en moyenne).

Avantages et inconvénients
  • Avantage : recherche, ajout et suppression très rapides (O(1) en moyenne)
  • Inconvénient : aucune garantie sur l'ordre des éléments
  • Inconvénient : nécessite que les éléments implémentent correctement hashCode() et equals()

2. Principe de la table de hachage

Table de hachage (Hash Table)

Une table de hachage est une structure de données qui utilise une fonction de hachage pour calculer un indice (appelé code de hachage) pour chaque objet. Cet indice détermine l'emplacement (appelé alvéole ou bucket) où l'objet sera stocké. Les objets avec des données différentes doivent générer des codes différents pour une distribution optimale.

En Java, les tables de hachage sont implémentées comme un tableau de listes chaînées (ou d'arbres binaires depuis Java 8).

Tableau de buckets Bucket 0 Bucket 1 Bucket 2 Bucket 3 Bucket 4 Bucket avec collisions Liste chaînée (collisions) Élément A Élément B Élément C Depuis Java 8 → Arbre équilibré après trop de collisions
Optimisation Java 8+

À partir de Java 8, les alvéoles (buckets) ne sont plus uniquement des listes chaînées. Lorsqu'une alvéole contient trop d'éléments (seuil par défaut : 8), elle est automatiquement convertie en arbre binaire équilibré (arbre rouge-noir). Cela améliore considérablement les performances en cas de collisions, passant de O(n) à O(log n).

3. Constructeurs de HashSet

Constructeur Description
HashSet<E>() Construit un HashSet vide avec une capacité initiale de 16 et un facteur de charge de 0,75.
HashSet<E>(Collection<? extends E> c) Construit un HashSet contenant les éléments de la collection spécifiée.
HashSet<E>(int capacity) Construit un HashSet vide avec la capacité initiale spécifiée.
HashSet<E>(int capacity, float loadFactor) Construit un HashSet vide avec la capacité initiale et le facteur de charge spécifiés.
Facteur de charge (load factor)

Le facteur de charge (0,75 par défaut) détermine le moment où la capacité de la table de hachage est augmentée. Lorsque le nombre d'éléments dépasse capacité × loadFactor, la table est redimensionnée (généralement doublée). Un facteur plus élevé économise de la mémoire mais dégrade les performances ; un facteur plus bas améliore les performances mais consomme plus de mémoire.

4. Méthodes principales

Méthode Description Complexité
boolean add(E e) Ajoute l'élément s'il n'est pas déjà présent. Retourne true si ajouté, false si déjà présent. O(1) amorti
boolean remove(Object o) Supprime l'élément spécifié s'il est présent. O(1) amorti
boolean contains(Object o) Retourne true si l'élément est présent. O(1) amorti
int size() Retourne le nombre d'éléments. O(1)
boolean isEmpty() Retourne true si l'ensemble est vide. O(1)
void clear() Supprime tous les éléments. O(n)
Iterator<E> iterator() Retourne un itérateur sur les éléments.

5. Exemples d'utilisation

  Exemple 1 — Opérations de base sur un HashSet

import java.util.HashSet;
import java.util.Iterator;

public class TestHashSet {
    public static void main(String[] args) {
        HashSet<String> pays = new HashSet<>();

        // Ajout d'éléments
        pays.add("Maroc");
        pays.add("Tunisie");
        pays.add("Algérie");
        pays.add("Espagne");
        pays.add("Malaisie");
        pays.add("Espagne");  // Tentative d'ajout en double → ignoré

        // Affichage (l'ordre n'est pas garanti)
        System.out.println("HashSet : " + pays);

        // Vérification d'appartenance
        System.out.println("Contient Tunisie ? " + pays.contains("Tunisie"));

        // Suppression
        pays.remove("Espagne");
        System.out.println("Après suppression : " + pays);

        // Parcours avec Iterator
        System.out.println("\n--- Parcours avec Iterator ---");
        Iterator<String> it = pays.iterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}
Sortie
HashSet : [Tunisie, Malaisie, Algérie, Maroc, Espagne]
Contient Tunisie ? true
Après suppression : [Tunisie, Malaisie, Algérie, Maroc]

--- Parcours avec Iterator ---
Tunisie
Malaisie
Algérie
Maroc

  Exemple 2 — HashSet à partir d'une autre collection

import java.util.HashSet;
import java.util.ArrayList;

public class TestHashSetFromCollection {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Maroc");
        list.add("Tunisie");
        list.add("Algérie");

        // Création d'un HashSet à partir de l'ArrayList
        HashSet<String> set = new HashSet<>(list);
        set.add("France");

        System.out.println("HashSet créé depuis ArrayList : " + set);
    }
}
Sortie
HashSet créé depuis ArrayList : [Tunisie, Algérie, Maroc, France]

6. Parcours d'un HashSet

  Exemple 3 — Différentes façons de parcourir

import java.util.HashSet;

public class ParcoursHashSet {
    public static void main(String[] args) {
        HashSet<String> fruits = new HashSet<>();
        fruits.add("Pomme");
        fruits.add("Banane");
        fruits.add("Orange");
        fruits.add("Mangue");

        // 1. Boucle for-each (recommandée)
        System.out.println("=== for-each ===");
        for (String fruit : fruits) {
            System.out.println(fruit);
        }

        // 2. Avec Iterator
        System.out.println("\n=== Iterator ===");
        var iterator = fruits.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        // 3. Avec forEach() + lambda (Java 8+)
        System.out.println("\n=== forEach() lambda ===");
        fruits.forEach(fruit -> System.out.println(fruit));

        // 4. Avec Stream (Java 8+)
        System.out.println("\n=== Stream ===");
        fruits.stream().sorted().forEach(System.out::println);
    }
}
Sortie
=== for-each ===
Pomme
Banane
Orange
Mangue

=== Iterator ===
Pomme
Banane
Orange
Mangue

=== forEach() lambda ===
Pomme
Banane
Orange
Mangue

=== Stream ===
Banane
Mangue
Orange
Pomme

7. HashSet avec objets personnalisés

Règle fondamentale

Pour que HashSet fonctionne correctement avec des objets personnalisés, vous devez redéfinir les méthodes hashCode() et equals(). Deux objets considérés comme égaux doivent avoir le même hashCode().

  Exemple 4 — HashSet d'étudiants

import java.util.HashSet;
import java.util.Objects;

class Etudiant {
    private int id;
    private String nom;
    private String prenom;

    public Etudiant(int id, String nom, String prenom) {
        this.id = id;
        this.nom = nom;
        this.prenom = prenom;
    }

    // Getters...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Etudiant etudiant = (Etudiant) o;
        return id == etudiant.id &&
               Objects.equals(nom, etudiant.nom) &&
               Objects.equals(prenom, etudiant.prenom);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, nom, prenom);
    }

    @Override
    public String toString() {
        return String.format("%s %s (id: %d)", prenom, nom, id);
    }
}

public class TestHashSetObjets {
    public static void main(String[] args) {
        HashSet<Etudiant> etudiants = new HashSet<>();

        etudiants.add(new Etudiant(1, "ESSADDOUKI", "Mostafa"));
        etudiants.add(new Etudiant(2, "KAYOUH", "Mohamed"));
        etudiants.add(new Etudiant(1, "ESSADDOUKI", "Mostafa"));  // Doublon → ignoré

        System.out.println("Nombre d'étudiants : " + etudiants.size());
        System.out.println(etudiants);
    }
}
Sortie
Nombre d'étudiants : 2
[Mostafa ESSADDOUKI (id: 1), Mohamed KAYOUH (id: 2)]

8. LinkedHashSet — HashSet avec ordre d'insertion

LinkedHashSet

LinkedHashSet est une sous-classe de HashSet qui maintient l'ordre d'insertion des éléments. Elle utilise une liste doublement chaînée pour lier les éléments dans l'ordre où ils ont été ajoutés.

  Exemple 5 — LinkedHashSet vs HashSet

import java.util.HashSet;
import java.util.LinkedHashSet;

public class CompareHashSetLinkedHashSet {
    public static void main(String[] args) {
        // HashSet → ordre non garanti
        HashSet<String> hashSet = new HashSet<>();
        hashSet.add("Z");
        hashSet.add("A");
        hashSet.add("M");
        hashSet.add("B");
        System.out.println("HashSet : " + hashSet);

        // LinkedHashSet → ordre d'insertion
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
        linkedHashSet.add("Z");
        linkedHashSet.add("A");
        linkedHashSet.add("M");
        linkedHashSet.add("B");
        System.out.println("LinkedHashSet : " + linkedHashSet);
    }
}
Sortie
HashSet : [A, B, Z, M]
LinkedHashSet : [Z, A, M, B]

9. TreeSet — Ensemble trié

TreeSet

TreeSet est une implémentation de Set basée sur un arbre rouge-noir. Les éléments sont stockés dans un ordre trié (naturel ou personnalisé via Comparator). Les opérations sont en O(log n).

Critère HashSet LinkedHashSet TreeSet
Ordre Aucun Ordre d'insertion Ordre trié (naturel ou Comparator)
Complexité O(1) amorti O(1) amorti O(log n)
Null autorisé Oui (un seul) Oui (un seul) Non (NullPointerException)
Mémoire Faible Moyenne (chaînage) Élevée (arbre)
Classe de base HashMap LinkedHashMap TreeMap

10. Exemple complet — Gestion d'une liste de contacts unique

  Exemple 6 — Annuaire sans doublons

import java.util.HashSet;
import java.util.Scanner;

class Contact {
    private String nom;
    private String telephone;

    public Contact(String nom, String telephone) {
        this.nom = nom;
        this.telephone = telephone;
    }

    public String getNom() { return nom; }
    public String getTelephone() { return telephone; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Contact contact = (Contact) o;
        return nom.equalsIgnoreCase(contact.nom) ||
               telephone.equals(contact.telephone);
    }

    @Override
    public int hashCode() {
        return Objects.hash(nom.toLowerCase(), telephone);
    }

    @Override
    public String toString() {
        return String.format("%s → %s", nom, telephone);
    }
}

public class AnnuaireSansDoublons {
    private HashSet<Contact> contacts;

    public AnnuaireSansDoublons() {
        contacts = new HashSet<>();
    }

    public boolean ajouterContact(String nom, String telephone) {
        Contact nouveau = new Contact(nom, telephone);
        if (contacts.add(nouveau)) {
            System.out.println("Contact ajouté : " + nouveau);
            return true;
        } else {
            System.out.println("Contact déjà existant (nom ou téléphone identique)");
            return false;
        }
    }

    public boolean supprimerContact(String nom) {
        Contact recherche = new Contact(nom, "");
        if (contacts.remove(recherche)) {
            System.out.println("Contact supprimé : " + nom);
            return true;
        } else {
            System.out.println("Contact non trouvé : " + nom);
            return false;
        }
    }

    public boolean rechercherContact(String nom) {
        for (Contact c : contacts) {
            if (c.getNom().equalsIgnoreCase(nom)) {
                System.out.println("Trouvé : " + c);
                return true;
            }
        }
        System.out.println("Contact non trouvé : " + nom);
        return false;
    }

    public void afficherTous() {
        if (contacts.isEmpty()) {
            System.out.println("Annuaire vide.");
            return;
        }
        System.out.println("\n=== ANNUAIRE (" + contacts.size() + " contact(s)) ===");
        for (Contact c : contacts) {
            System.out.println("  " + c);
        }
        System.out.println();
    }

    public static void main(String[] args) {
        AnnuaireSansDoublons annuaire = new AnnuaireSansDoublons();
        Scanner scanner = new Scanner(System.in);
        int choix;

        do {
            System.out.println("\n╔════════════════════════════════════╗");
            System.out.println("║     ANNUAIRE SANS DOUBLONS         ║");
            System.out.println("╠════════════════════════════════════╣");
            System.out.println("║ 1. Ajouter un contact              ║");
            System.out.println("║ 2. Supprimer un contact            ║");
            System.out.println("║ 3. Rechercher un contact           ║");
            System.out.println("║ 4. Afficher tous les contacts      ║");
            System.out.println("║ 5. Quitter                         ║");
            System.out.println("╚════════════════════════════════════╝");
            System.out.print("Votre choix : ");
            choix = scanner.nextInt();
            scanner.nextLine();

            switch (choix) {
                case 1:
                    System.out.print("Nom : ");
                    String nom = scanner.nextLine();
                    System.out.print("Téléphone : ");
                    String tel = scanner.nextLine();
                    annuaire.ajouterContact(nom, tel);
                    break;
                case 2:
                    System.out.print("Nom : ");
                    nom = scanner.nextLine();
                    annuaire.supprimerContact(nom);
                    break;
                case 3:
                    System.out.print("Nom : ");
                    nom = scanner.nextLine();
                    annuaire.rechercherContact(nom);
                    break;
                case 4:
                    annuaire.afficherTous();
                    break;
                case 5:
                    System.out.println("Au revoir !");
                    break;
                default:
                    System.out.println("Choix invalide !");
            }
        } while (choix != 5);

        scanner.close();
    }
}

11. Exercice

Détection de doublons

Niveau : Intermédiaire

Écrire un programme qui lit une liste de mots et détecte les doublons.

Travail demandé

  1. Lire une phrase ou une liste de mots.
  2. Utiliser un HashSet pour stocker les mots uniques.
  3. Utiliser un HashSet supplémentaire pour détecter les doublons.
  4. Afficher :
    • La liste des mots sans doublons
    • La liste des mots en double
    • Le nombre de mots uniques

  L'essentiel en bref

Synthèse — HashSet et ensembles en Java
  • HashSet implémente un ensemble sans doublons basé sur une table de hachage.
  • Complexité : O(1) pour add(), remove() et contains() en moyenne.
  • L'ordre des éléments n'est pas garanti (contrairement à LinkedHashSet).
  • Constructeurs : capacité initiale et facteur de charge paramétrables.
  • Méthodes principales : add(), remove(), contains(), size(), isEmpty(), clear().
  • Parcours : for-each, Iterator, forEach() lambda.
  • Pour les objets personnalisés : hashCode() et equals() doivent être redéfinis.
  • Optimisation Java 8+ : conversion automatique des listes chaînées en arbres équilibrés après trop de collisions.
  • LinkedHashSet : conserve l'ordre d'insertion.
  • TreeSet : conserve l'ordre trié (O(log n)).
Un peu d'histoire

Le concept de table de hachage a été inventé en 1953 par Hans Peter Luhn chez IBM. En Java, HashSet a été introduite dans Java 1.2 (1998) avec le Java Collections Framework. L'optimisation majeure de Java 8 (2014) convertit dynamiquement les listes chaînées en arbres rouges-noirs lorsqu'un bucket dépasse le seuil de 8 éléments, garantissant une complexité O(log n) dans le pire cas au lieu de O(n). Cette amélioration protège contre les attaques par déni de service basées sur des collisions de hachage malveillantes.

Sortie
// La sortie apparaîtra ici…
Prêt · Ctrl+Entrée pour exécuter

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.