Une application doit souvent associer une information à une clé unique, comme un identifiant client à son profil ou un mot à son nombre d’occurrences. La HashMap en Java répond précisément à ce besoin avec des recherches généralement très rapides, à condition de bien comprendre les clés, les collisions, l’ordre des éléments et les accès concurrents. Je détaille ici son fonctionnement, ses méthodes utiles, ses limites et les situations où une autre implémentation de Map sera plus adaptée.
Les points essentiels à retenir sur HashMap
- Association clé-valeur : chaque clé identifie au maximum une valeur.
-
Performances :
getetputoffrent généralement un coût moyen en O(1). - Ordre imprévisible : HashMap ne garantit pas l’ordre d’insertion ni un ordre stable.
-
Valeurs nulles autorisées : une clé
nullet plusieurs valeursnullsont possibles. - Pas thread-safe : pour des accès concurrents, il faut une synchronisation ou une autre collection.
Pourquoi utiliser une HashMap en Java
Je choisis généralement cette structure lorsque je dois retrouver rapidement une valeur à partir d’une clé. Un dictionnaire de configuration, un cache local, un index par identifiant ou un compteur de mots sont des cas d’usage classiques. L’idée est simple, mais très efficace : au lieu de parcourir toute une liste, Java calcule l’emplacement probable de la clé.
Map scores = new HashMap<>();
scores.put("Alice", 18);
scores.put("Marc", 15);
int score = scores.get("Alice");
System.out.println(score); // 18
Les clés sont uniques. Si j’exécute une seconde fois scores.put("Alice", 20), la valeur 18 est remplacée par 20. Cette règle est utile pour mettre à jour un état, mais elle provoque aussi des erreurs lorsque l’on pensait ajouter une nouvelle entrée.
Une HashMap autorise les types génériques, ce qui permet de contrôler clairement le type des clés et des valeurs. Elle accepte également une clé null et plusieurs valeurs null, contrairement à ConcurrentHashMap.
Ce que HashMap ne garantit pas
L’ordre d’affichage n’est pas défini. Une boucle sur entrySet() peut produire un ordre différent après une modification, un redimensionnement ou un changement de version de Java. Pour conserver l’ordre d’insertion, j’utilise plutôt LinkedHashMap.
La rapidité annoncée reste une moyenne. Des clés dont les méthodes hashCode() produisent souvent le même résultat entraînent davantage de collisions et peuvent ralentir les recherches. Une bonne dispersion des hash codes est donc une condition importante.

Comment une HashMap retrouve une valeur
Lorsqu’une entrée est ajoutée, Java utilise le hashCode() de la clé pour déterminer un compartiment, souvent appelé bucket. Lors d’un appel à get, la même logique est appliquée, puis Java vérifie l’égalité réelle avec equals() afin de retrouver la bonne clé.
Le contrat entre hashCode et equals
Pour une classe personnalisée utilisée comme clé, je considère la redéfinition cohérente de equals() et hashCode() comme indispensable. Deux objets égaux doivent toujours produire le même hash code. Si cette règle est violée, une entrée peut être ajoutée mais devenir impossible à retrouver.
final class Utilisateur {
private final int id;
Utilisateur(int id) {
this.id = id;
}
@Override
public boolean equals(Object objet) {
if (this == objet) return true;
if (!(objet instanceof Utilisateur autre)) return false;
return id == autre.id;
}
@Override
public int hashCode() {
return Integer.hashCode(id);
}
}
J’évite aussi les clés mutables. Si un champ utilisé par equals() ou hashCode() change après l’insertion, la HashMap peut chercher l’objet dans un compartiment différent de celui où il se trouve réellement. Le résultat ressemble alors à une disparition de données, alors que l’entrée existe encore en mémoire.
Capacité et facteur de charge
Une HashMap possède une capacité initiale et un facteur de charge. Par défaut, la capacité initiale est généralement de 16 compartiments et le facteur de charge de 0,75. Lorsque le nombre d’entrées dépasse le seuil calculé, la table est agrandie et les entrées sont redistribuées.
Pour une collection dont la taille finale est connue, je préfère prévoir une capacité suffisante. Cela limite les opérations de redimensionnement, surtout lors du chargement de milliers d’éléments. Dans Java 19 et les versions suivantes, HashMap.newHashMap(nombreAttendu) rend cette intention plus claire.
HashMap produits =
HashMap.newHashMap(500);
Il ne faut cependant pas surdimensionner systématiquement la structure. L’itération dépend de la capacité de la table et du nombre d’éléments, donc une capacité inutilement élevée peut augmenter le coût de parcours et la mémoire consommée.
Les opérations qui servent vraiment au quotidien
Les méthodes de base suffisent pour la plupart des cas. Je recommande de privilégier les vues et les opérations de la famille Map plutôt que de manipuler des détails internes.
| Méthode | Utilité | Point d’attention |
|---|---|---|
put(k, v) |
Ajoute ou remplace une valeur | Une clé existante est écrasée |
get(k) |
Récupère une valeur |
null peut signifier absence ou valeur nulle |
containsKey(k) |
Vérifie la présence d’une clé | À utiliser avant get si null est accepté |
remove(k) |
Supprime une association | Ne supprime que la clé indiquée |
getOrDefault(k, v) |
Retourne une valeur de secours | Ne modifie pas la map |
computeIfAbsent |
Crée une valeur uniquement si nécessaire | La fonction ne doit pas retourner null pour insérer |
Éviter l’ambiguïté avec null
Le code suivant ne permet pas de distinguer une clé absente d’une clé associée à null.
if (map.get("theme") == null) {
// La clé est-elle absente ou sa valeur vaut-elle null ?
}
Lorsque la distinction compte, j’utilise containsKey ou getOrDefault. Cette petite précaution évite des bugs discrets dans les paramètres, les profils utilisateur et les résultats de cache.
Compter et regrouper avec computeIfAbsent
Pour regrouper des objets par catégorie, computeIfAbsent évite de répéter la vérification d’existence de la liste.
Map> parLangue = new HashMap<>();
parLangue
.computeIfAbsent("français", cle -> new ArrayList<>())
.add("article technique");
Pour compter des occurrences, merge est souvent encore plus lisible.
Map occurrences = new HashMap<>();
occurrences.merge("java", 1, Integer::sum);
occurrences.merge("java", 1, Integer::sum);
Dans cet exemple, la valeur associée à java devient 2. Je trouve cette approche plus sûre qu’un enchaînement manuel de get, de test et de put.
Quelle implémentation de Map choisir
HashMap est un excellent choix par défaut, mais elle n’est pas universelle. Le bon choix dépend principalement de l’ordre attendu, du besoin de tri et du nombre de threads qui accèdent aux données.
| Structure | Ordre | Accès concurrents | Cas d’usage |
|---|---|---|---|
HashMap |
Aucun ordre garanti | Non sécurisée | Stockage général rapide dans un contexte local |
LinkedHashMap |
Insertion ou accès | Non sécurisée | Résultats prévisibles, cache LRU simple |
TreeMap |
Clés triées | Non sécurisée | Recherche par ordre et intervalles de clés |
ConcurrentHashMap |
Aucun ordre garanti | Oui | État partagé entre plusieurs threads |
TreeMap est intéressant lorsque je dois obtenir les clés dans l’ordre ou exploiter des méthodes comme firstKey et subMap. Son coût moyen est en O(log n), ce qui est différent de la recherche généralement constante d’une HashMap.
Pour une map partagée entre plusieurs threads, je préfère ConcurrentHashMap. Elle fournit des opérations atomiques comme putIfAbsent et computeIfAbsent, mais n’accepte ni clé null ni valeur null. Une simple enveloppe Collections.synchronizedMap peut convenir dans un cas simple, mais les parcours de ses vues doivent eux aussi être protégés correctement.
Les erreurs qui coûtent cher
Supposer que l’ordre est stable
Afficher directement une HashMap dans une interface ou comparer deux sorties textuelles est risqué. Même si l’ordre semble stable pendant plusieurs exécutions, il ne fait pas partie du contrat. Pour un affichage déterministe, je copie les données dans une LinkedHashMap ou je trie explicitement les entrées.
Modifier la map pendant un parcours
Une modification structurelle pendant une boucle sur entrySet peut provoquer une ConcurrentModificationException. Le bon réflexe consiste à utiliser l’itérateur pour supprimer l’élément courant, ou à employer removeIf lorsque l’expression reste claire.
map.entrySet().removeIf(
entree -> entree.getValue() < 0
);
Le comportement fail-fast aide à détecter un problème, mais il ne constitue pas un mécanisme de sécurité concurrente. Je ne base jamais la logique métier sur l’apparition garantie de cette exception.
Utiliser une HashMap dans plusieurs threads sans protection
HashMap n’est pas synchronisée. Si plusieurs threads la lisent et qu’au moins l’un d’eux la modifie, le programme doit organiser explicitement les accès. Une déclaration comme final Map ne rend pas la structure sûre par elle-même.
Dans une application concurrente, je choisis soit une protection par verrou adaptée, soit ConcurrentHashMap. Le choix dépend de la granularité des opérations et du besoin d’effectuer plusieurs actions comme une seule étape atomique.
Lire aussi : Java Array - Maîtrisez les tableaux sans erreur !
Calculer une capacité au hasard
Une capacité de 100 000 n’est pas automatiquement meilleure qu’une capacité de 16. Elle n’a de sens que si le volume attendu est proche de cette valeur. Pour éviter les redimensionnements sans gaspiller de mémoire, je pars du nombre d’entrées prévu et du facteur de charge de 0,75.
Le bon réflexe avant de valider son choix
Pour une association simple clé-valeur dans un seul thread, je commence par HashMap. Je vérifie ensuite trois points très concrets : la clé est-elle stable, l’ordre est-il sans importance et la taille attendue justifie-t-elle une capacité initiale particulière ?
Si l’ordre compte, je prends LinkedHashMap. Si les clés doivent rester triées, je prends TreeMap. Si plusieurs threads modifient la structure, je passe à ConcurrentHashMap plutôt que de compter sur une HashMap ordinaire.
Cette décision simple évite la plupart des problèmes rencontrés en pratique. Une HashMap bien utilisée reste l’un des outils les plus polyvalents de Java, à condition de respecter le contrat equals/hashCode, de ne pas attendre un ordre implicite et de traiter correctement la question de la concurrence.