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 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).
- 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()etequals()
2. Principe de la table de hachage
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).
À 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. |
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());
}
}
}
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);
}
}
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);
}
}
=== 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
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);
}
}
Nombre d'étudiants : 2 [Mostafa ESSADDOUKI (id: 1), Mohamed KAYOUH (id: 2)]
8. LinkedHashSet — HashSet avec ordre d'insertion
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);
}
}
HashSet : [A, B, Z, M] LinkedHashSet : [Z, A, M, B]
9. TreeSet — Ensemble trié
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
Écrire un programme qui lit une liste de mots et détecte les doublons.
Travail demandé
- Lire une phrase ou une liste de mots.
- Utiliser un
HashSetpour stocker les mots uniques. - Utiliser un
HashSetsupplémentaire pour détecter les doublons. - Afficher :
- La liste des mots sans doublons
- La liste des mots en double
- Le nombre de mots uniques
import java.util.HashSet;
import java.util.Scanner;
public class DetecteurDoublons {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Entrez une liste de mots séparés par des espaces :");
String ligne = scanner.nextLine();
// Nettoyage et découpage
String[] mots = ligne.toLowerCase().split("\\s+");
HashSet<String> motsUniques = new HashSet<>();
HashSet<String> motsDoublons = new HashSet<>();
// Détection des doublons
for (String mot : mots) {
if (!motsUniques.add(mot)) {
// Si add() retourne false, l'élément existe déjà
motsDoublons.add(mot);
}
}
// Affichage des résultats
System.out.println("\n=== RÉSULTATS ===");
System.out.println("Nombre total de mots : " + mots.length);
System.out.println("Nombre de mots uniques : " + motsUniques.size());
System.out.println("\n--- Mots sans doublons ---");
for (String mot : motsUniques) {
System.out.print(mot + " ");
}
if (!motsDoublons.isEmpty()) {
System.out.println("\n\n--- Mots en double ---");
for (String mot : motsDoublons) {
System.out.print(mot + " ");
}
} else {
System.out.println("\n\nAucun doublon détecté !");
}
scanner.close();
}
}
Entrez une liste de mots séparés par des espaces : le chat et le chien jouent et le chat dort === RÉSULTATS === Nombre total de mots : 11 Nombre de mots uniques : 7 --- Mots sans doublons --- chat et jouent chien le dort --- Mots en double --- chat le et
HashSet.add()retournefalsesi l'élément existe déjà — pratique pour détecter les doublons.- Deux
HashSet: un pour les éléments uniques, un pour les doublons détectés. - Le nettoyage (minuscules, découpage) garantit une comparaison cohérente.
L'essentiel en bref
HashSetimplémente un ensemble sans doublons basé sur une table de hachage.- Complexité : O(1) pour
add(),remove()etcontains()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()etequals()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)).
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.
Discussion (0)
Soyez le premier à laisser un commentaire !
Laisser un commentaire
Votre commentaire sera visible après modération.