HashMap — Tables de hachage en Java
Prérequis
Maîtriser les collections Java et les génériques. Comprendre le concept de clé/valeur. Connaître les méthodes hashCode() et equals().
Objectifs
Comprendre le principe des tables de hachage et leur implémentation avec HashMap. Maîtriser les opérations d'insertion, de recherche et de suppression. Connaître les bonnes pratiques (clés immutables, redéfinition de hashCode()/equals()).
1. Qu'est-ce qu'un HashMap ?
HashMap est une structure de données qui stocke des paires clé/valeur (associations). Chaque clé est unique et permet de retrouver rapidement la valeur associée. La recherche se fait par la clé, pas par une copie exacte de l'objet (contrairement à Set).
Exemple d'utilisation : stocker des employés avec leur identifiant comme clé.
HashMap<K, V> map = new HashMap<>();
// Avec var (Java 10+)
var map = new HashMap<K, V>();
Exemple 1 — Stockage d'employés
import java.util.HashMap;
class Employe {
private Integer id;
private String nom;
private String prenom;
public Employe(Integer id, String nom, String prenom) {
this.id = id;
this.nom = nom;
this.prenom = prenom;
}
@Override
public String toString() {
return "Employe [id=" + id + ", nom=" + nom + ", prenom=" + prenom + "]";
}
}
public class Test {
public static void main(String[] args) {
var employes = new HashMap<Integer, Employe>();
employes.put(1, new Employe(1, "ESSADDOUKI", "Mostafa"));
employes.put(4, new Employe(4, "KAYOUH", "Mohamed"));
employes.put(7, new Employe(7, "Moutawakil", "Dounia"));
System.out.println("HashMap : " + employes);
}
}
HashMap : {1=Employe [id=1, nom=ESSADDOUKI, prenom=Mostafa], 4=Employe [id=4, nom=KAYOUH, prenom=Mohamed], 7=Employe [id=7, nom=Moutawakil, prenom=Dounia]}
2. Constructeurs de HashMap
| Constructeur | Description |
|---|---|
HashMap<K, V>() |
Capacité initiale 16, facteur de charge 0.75 |
HashMap<K, V>(int capacity) |
Capacité initiale spécifiée, facteur de charge 0.75 |
HashMap<K, V>(int capacity, float loadFactor) |
Capacité et facteur de charge spécifiés |
HashMap<K, V>(Map<? extends K, ? extends V> m) |
Crée un HashMap à partir d'une autre Map |
Le facteur de charge (0.75 par défaut) contrôle le moment où la table de hachage est agrandie. Un facteur plus élevé économise de la mémoire mais réduit les performances. Un facteur plus bas améliore les performances mais consomme plus de mémoire.
3. Méthodes principales
| Méthode | Description | Complexité |
|---|---|---|
V put(K key, V value) |
Insère une paire clé/valeur (remplace si clé existante) | O(1) amorti |
V get(Object key) |
Retourne la valeur associée à la clé (ou null) | O(1) amorti |
V remove(Object key) |
Supprime la paire associée à la clé | O(1) amorti |
boolean containsKey(Object key) |
Vérifie si la clé existe | O(1) amorti |
boolean containsValue(Object value) |
Vérifie si la valeur existe | O(n) |
int size() |
Nombre de paires clé/valeur | O(1) |
boolean isEmpty() |
Vérifie si la map est vide | O(1) |
void clear() |
Supprime toutes les entrées | O(n) |
Set<K> keySet() |
Retourne un ensemble des clés | O(1) |
Collection<V> values() |
Retourne une collection des valeurs | O(1) |
Set<Map.Entry<K,V>> entrySet() |
Retourne un ensemble des entrées | O(1) |
4. Opérations de base
Exemple 2 — Manipulation d'un HashMap
import java.util.HashMap;
class Employe {
private Integer id;
private String nom;
private String prenom;
public Employe(Integer id, String nom, String prenom) {
this.id = id;
this.nom = nom;
this.prenom = prenom;
}
@Override
public String toString() {
return "Employe [id=" + id + ", nom=" + nom + ", prenom=" + prenom + "]";
}
}
public class Test {
public static void main(String[] args) {
var employes = new HashMap<Integer, Employe>();
// Ajout d'éléments
employes.put(1, new Employe(1, "ESSADDOUKI", "Mostafa"));
employes.put(4, new Employe(4, "KAYOUH", "Mohamed"));
employes.put(7, new Employe(7, "Moutawakil", "Dounia"));
System.out.println("Taille : " + employes.size());
// Vérification et récupération
if (employes.containsKey(4)) {
Employe emp = employes.get(4);
System.out.println("Employé avec clé 4 : " + emp);
}
// Affichage de toutes les valeurs
System.out.println("Tous les employés : " + employes.values());
// Suppression de toutes les entrées
employes.clear();
if (employes.isEmpty()) {
System.out.println("HashMap est vide");
}
}
}
Taille : 3 Employé avec clé 4 : Employe [id=4, nom=KAYOUH, prenom=Mohamed] Tous les employés : [Employe [id=1, nom=ESSADDOUKI, prenom=Mostafa], Employe [id=4, nom=KAYOUH, prenom=Mohamed], Employe [id=7, nom=Moutawakil, prenom=Dounia]] HashMap est vide
5. Parcours d'un HashMap
Exemple 3 — Différentes façons de parcourir
import java.util.HashMap;
import java.util.Map;
public class ParcoursHashMap {
public static void main(String[] args) {
HashMap<String, Integer> notes = new HashMap<>();
notes.put("Mostafa", 18);
notes.put("Amal", 16);
notes.put("Youssef", 14);
notes.put("Fatima", 17);
// 1. Parcours des clés
System.out.println("=== Parcours des clés ===");
for (String nom : notes.keySet()) {
System.out.println("Clé : " + nom);
}
// 2. Parcours des valeurs
System.out.println("\n=== Parcours des valeurs ===");
for (Integer note : notes.values()) {
System.out.println("Valeur : " + note);
}
// 3. Parcours des entrées (clé + valeur) — recommandé
System.out.println("\n=== Parcours des entrées ===");
for (Map.Entry<String, Integer> entry : notes.entrySet()) {
System.out.println(entry.getKey() + " → " + entry.getValue());
}
// 4. Avec forEach() + lambda (Java 8+)
System.out.println("\n=== forEach() avec lambda ===");
notes.forEach((nom, note) -> System.out.println(nom + " → " + note));
}
}
=== Parcours des clés === Clé : Mostafa Clé : Amal Clé : Youssef Clé : Fatima === Parcours des valeurs === Valeur : 18 Valeur : 16 Valeur : 14 Valeur : 17 === Parcours des entrées === Mostafa → 18 Amal → 16 Youssef → 14 Fatima → 17 === forEach() avec lambda === Mostafa → 18 Amal → 16 Youssef → 14 Fatima → 17
6. Méthodes spécifiques (Java 8+)
Exemple 4 — Méthodes modernes
import java.util.HashMap;
public class MethodesModernes {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
// putIfAbsent — ajoute seulement si la clé n'existe pas
map.putIfAbsent("A", 10);
map.putIfAbsent("A", 20); // Ignoré
System.out.println("putIfAbsent : " + map);
// computeIfAbsent — calcule la valeur si la clé est absente
map.computeIfAbsent("B", k -> k.length() * 5);
System.out.println("computeIfAbsent : " + map);
// computeIfPresent — modifie la valeur si la clé existe
map.computeIfPresent("A", (k, v) -> v + 100);
System.out.println("computeIfPresent : " + map);
// getOrDefault — retourne une valeur par défaut si clé absente
Integer valeur = map.getOrDefault("Z", 0);
System.out.println("getOrDefault('Z') : " + valeur);
// merge — fusionne les valeurs
map.merge("A", 5, (ancienne, nouvelle) -> ancienne + nouvelle);
System.out.println("merge('A', 5) : " + map);
}
}
putIfAbsent : {A=10}
computeIfAbsent : {A=10, B=5}
computeIfPresent : {A=110, B=5}
getOrDefault('Z') : 0
merge('A', 5) : {A=115, B=5}
7. Clés personnalisées — hashCode() et equals()
Pour utiliser des objets personnalisés comme clés dans un HashMap, vous devez redéfinir correctement les méthodes hashCode() et equals(). Deux objets considérés comme égaux (equals() true) doivent avoir le même hashCode().
Exemple 5 — Clé personnalisée
import java.util.HashMap;
import java.util.Objects;
class Client {
private int id;
private String email;
public Client(int id, String email) {
this.id = id;
this.email = email;
}
// Getters, setters...
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Client client = (Client) o;
return id == client.id && Objects.equals(email, client.email);
}
@Override
public int hashCode() {
return Objects.hash(id, email);
}
@Override
public String toString() {
return "Client{id=" + id + ", email='" + email + "'}";
}
}
public class TestClePersonnalisee {
public static void main(String[] args) {
HashMap<Client, String> commandes = new HashMap<>();
Client c1 = new Client(1, "mostafa@email.com");
Client c2 = new Client(2, "amal@email.com");
commandes.put(c1, "Commande #1234");
commandes.put(c2, "Commande #5678");
// Recherche avec un objet équivalent
Client recherche = new Client(1, "mostafa@email.com");
String commande = commandes.get(recherche);
System.out.println("Commande trouvée : " + commande);
}
}
Commande trouvée : Commande #1234
8. HashMap vs Hashtable vs ConcurrentHashMap
| Caractéristique | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| Synchronisation | Non (non thread-safe) | Oui (thread-safe) | Oui (thread-safe, performances améliorées) |
| Clés/ valeurs nulles | Oui (une clé null, plusieurs valeurs null) | Non | Non |
| Performance | Élevée (pas de synchronisation) | Faible (synchronisation globale) | Bonne (synchronisation segmentée) |
| Ordre d'itération | Non garanti | Non garanti | Non garanti |
| Depuis | Java 1.2 | Java 1.0 (dépréciée) | Java 5 |
Hashtable est une ancienne classe synchronisée (thread-safe) mais déconseillée. Pour un accès concurrent, utilisez ConcurrentHashMap. Pour un usage mono-thread, HashMap est plus performant.
9. Exemple complet — Annuaire téléphonique
Exemple 6 — Application d'annuaire
import java.util.HashMap;
import java.util.Scanner;
public class Annuaire {
private HashMap<String, String> contacts;
public Annuaire() {
contacts = new HashMap<>();
}
public void ajouterContact(String nom, String telephone) {
contacts.put(nom, telephone);
System.out.println("Contact ajouté : " + nom + " → " + telephone);
}
public void rechercherParNom(String nom) {
String telephone = contacts.get(nom);
if (telephone != null) {
System.out.println("" + nom + " : " + telephone);
} else {
System.out.println("Contact non trouvé : " + nom);
}
}
public void supprimerContact(String nom) {
String supprime = contacts.remove(nom);
if (supprime != null) {
System.out.println("Contact supprimé : " + nom);
} else {
System.out.println("Contact non trouvé : " + nom);
}
}
public void modifierContact(String nom, String nouveauTelephone) {
if (contacts.containsKey(nom)) {
contacts.put(nom, nouveauTelephone);
System.out.println("Contact modifié : " + nom + " → " + nouveauTelephone);
} else {
System.out.println("Contact non trouvé : " + nom);
}
}
public void afficherTous() {
if (contacts.isEmpty()) {
System.out.println("Annuaire vide.");
return;
}
System.out.println("\n=== ANNUAIRE ===");
contacts.forEach((nom, tel) -> System.out.printf(" %s → %s%n", nom, tel));
System.out.println("Total : " + contacts.size() + " contact(s)\n");
}
public static void main(String[] args) {
Annuaire annuaire = new Annuaire();
Scanner scanner = new Scanner(System.in);
int choix;
do {
System.out.println("\n╔════════════════════════════════════╗");
System.out.println("║ ANNUAIRE TÉLÉPHONIQUE ║");
System.out.println("╠════════════════════════════════════╣");
System.out.println("║ 1. Ajouter un contact ║");
System.out.println("║ 2. Rechercher par nom ║");
System.out.println("║ 3. Supprimer un contact ║");
System.out.println("║ 4. Modifier un contact ║");
System.out.println("║ 5. Afficher tous les contacts ║");
System.out.println("║ 6. 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.rechercherParNom(nom);
break;
case 3:
System.out.print("Nom : ");
nom = scanner.nextLine();
annuaire.supprimerContact(nom);
break;
case 4:
System.out.print("Nom : ");
nom = scanner.nextLine();
System.out.print("Nouveau téléphone : ");
tel = scanner.nextLine();
annuaire.modifierContact(nom, tel);
break;
case 5:
annuaire.afficherTous();
break;
case 6:
System.out.println("Au revoir !");
break;
default:
System.out.println("Choix invalide !");
}
} while (choix != 6);
scanner.close();
}
}
10. Exercice
Compteur de mots
Créer un programme qui compte la fréquence d'apparition de chaque mot dans un texte.
Travail demandé
- Lire une phrase ou un texte saisi par l'utilisateur.
- Utiliser un
HashMap<String, Integer>pour stocker chaque mot et son nombre d'occurrences. - Ignorer la casse (convertir en minuscules).
- Ignorer la ponctuation (virgules, points, points d'exclamation, etc.).
- Afficher les mots triés par ordre alphabétique ou par fréquence décroissante.
- Afficher le nombre total de mots et le nombre de mots distincts.
import java.util.*;
public class CompteurDeMots {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("Entrez votre texte :");
String texte = scanner.nextLine();
// Nettoyage et découpage
String textePropre = texte.toLowerCase()
.replaceAll("[^a-zàâäéèêëîïôöùûüç\\s]", "");
String[] mots = textePropre.split("\\s+");
// Comptage des fréquences
HashMap<String, Integer> frequences = new HashMap<>();
for (String mot : mots) {
if (!mot.isEmpty()) {
frequences.put(mot, frequences.getOrDefault(mot, 0) + 1);
}
}
// Affichage des résultats
System.out.println("\n=== STATISTIQUES ===");
System.out.println("Nombre total de mots : " + mots.length);
System.out.println("Nombre de mots distincts : " + frequences.size());
// Tri par ordre alphabétique
System.out.println("\n--- Par ordre alphabétique ---");
List<String> motsTries = new ArrayList<>(frequences.keySet());
Collections.sort(motsTries);
for (String mot : motsTries) {
System.out.printf(" %-15s → %d fois%n", mot, frequences.get(mot));
}
// Tri par fréquence décroissante
System.out.println("\n--- Par fréquence décroissante ---");
List<Map.Entry<String, Integer>> entries = new ArrayList<>(frequences.entrySet());
entries.sort((e1, e2) -> e2.getValue().compareTo(e1.getValue()));
for (Map.Entry<String, Integer> entry : entries) {
System.out.printf(" %-15s → %d fois%n", entry.getKey(), entry.getValue());
}
// Mot le plus fréquent
if (!entries.isEmpty()) {
Map.Entry<String, Integer> plusFrequent = entries.get(0);
System.out.println("\nMot le plus fréquent : '" + plusFrequent.getKey()
+ "' (" + plusFrequent.getValue() + " fois)");
}
scanner.close();
}
}
Entrez votre texte : Le chat et le chien jouent ensemble. Le chat est heureux, le chien aussi. === STATISTIQUES === Nombre total de mots : 13 Nombre de mots distincts : 9 --- Par ordre alphabétique --- aussi → 1 fois chat → 2 fois chien → 1 fois ensemble → 1 fois est → 1 fois heureux → 1 fois jouent → 1 fois le → 4 fois et → 1 fois --- Par fréquence décroissante --- le → 4 fois chat → 2 fois aussi → 1 fois chien → 1 fois ensemble → 1 fois est → 1 fois heureux → 1 fois jouent → 1 fois et → 1 fois Mot le plus fréquent : 'le' (4 fois)
getOrDefault(): simplifie l'incrémentation des compteurs.replaceAll()+ regex : nettoyage du texte (ponctuation, minuscules).- Tri avec
Collections.sort(): pour l'ordre alphabétique. - Tri personnalisé avec
Comparator: pour l'ordre par fréquence.
L'essentiel en bref
HashMapstocke des paires clé/valeur avec recherche par clé en O(1) amorti.- Création :
HashMap<K, V> map = new HashMap<>(); - Insertion :
put(key, value)— remplace si la clé existe déjà. - Récupération :
get(key)— retourne null si la clé n'existe pas. - Vérification :
containsKey(key)(efficace) oucontainsValue(value)(lent). - Suppression :
remove(key). - Clés personnalisées : doivent redéfinir
hashCode()etequals(). - Non thread-safe : utilisez
ConcurrentHashMappour la concurrence. - Une clé null autorisée, plusieurs valeurs null autorisées.
- Parcours :
keySet(),values(),entrySet()ouforEach()avec lambda.
Le concept de table de hachage a été inventé en 1953 par Hans Peter Luhn chez IBM. En Java, HashMap a été introduite dans Java 1.2 (1998) avec le Java Collections Framework. Java 8 a amélioré les performances en cas de collisions de hachage : les listes chaînées sont automatiquement converties en arbres équilibrés (arbres rouges-noirs) lorsqu'un bucket dépasse un seuil (8 éléments). Cette optimisation garantit une complexité O(log n) dans le pire cas, au lieu de O(n) auparavant.
Discussion (0)
Soyez le premier à laisser un commentaire !
Laisser un commentaire
Votre commentaire sera visible après modération.