HashMap en Java, les clés pour éviter les pièges courants

Noël Besnard .

1 octobre 2026

Représentation d'un HashMap Java : une clé (Key1:Value1) pointe vers une structure arborescente contenant d'autres clés (Key2:Value2, Key3:Value3, Key4:Value4).

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 : get et put offrent 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é null et plusieurs valeurs null sont 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.

Structure interne d'un hashmap Java : un tableau de buckets, chacun pouvant contenir une liste chaînée de nœuds.

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 map = new HashMap<>() 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.

Questions fréquentes

HashMap utilise le hashCode de la clé pour déterminer un compartiment, puis equals pour vérifier la clé exacte. Le coût moyen est généralement en O(1), mais des collisions fréquentes peuvent ralentir les recherches.
La méthode get renvoie null dans les deux cas. Utilisez containsKey pour vérifier la présence de la clé, ou getOrDefault pour fournir une valeur de secours sans modifier la map.
Utilisez LinkedHashMap pour conserver l’ordre d’insertion ou d’accès, TreeMap pour trier les clés, et ConcurrentHashMap lorsque plusieurs threads modifient la structure. ConcurrentHashMap n’accepte ni clé null ni valeur null.
La capacité initiale est généralement de 16 compartiments et le facteur de charge de 0,75. Si le nombre final d’entrées est connu, prévoyez une capacité suffisante, par exemple avec HashMap.newHashMap(nombreAttendu) dans Java 19 et les versions suivantes.
Évaluer l'article

Moyenne: 5.0 / 5 · 1 évaluation

Tags

hashmap concurrence linkedhashmap treemap collisions
Autor Noël Besnard
Noël Besnard
Je m'appelle Noël Besnard et je possède 7 ans d'expérience dans le domaine de la technologie, en particulier en ce qui concerne le web, l'intelligence artificielle, les réseaux et la sécurité. Mon intérêt pour ces sujets a commencé dès mon adolescence, lorsque j'ai découvert le potentiel incroyable des nouvelles technologies pour transformer notre quotidien. J'aime explorer les défis que posent ces avancées et partager des solutions accessibles pour aider les lecteurs à mieux comprendre ces enjeux. Au fil des ans, j'ai eu l'occasion d'écrire sur divers aspects de la tech, en mettant l'accent sur la clarté et la précision. Je m'efforce de vérifier mes sources et de comparer les informations afin de proposer un contenu à jour et fiable. Mon objectif est de rendre des sujets complexes plus simples et compréhensibles, tout en suivant les dernières tendances du secteur. Je suis ravi de partager mes connaissances et d'accompagner les lecteurs dans leur exploration des technologies qui façonnent notre avenir.
Commentaires (0)
Ajouter un commentaire