Quand une application doit relier un identifiant à une valeur, une ville à son code postal ou un utilisateur à ses droits, une simple liste devient vite peu pratique. Une carte Java permet de retrouver chaque donnée à partir d’une clé, mais le choix entre HashMap, LinkedHashMap, TreeMap et les variantes concurrentes change beaucoup le comportement du programme. Je présente ici les bases, les différences concrètes, les erreurs fréquentes et les bonnes pratiques utiles dans un projet réel.
La bonne implémentation dépend surtout de l’usage prévu
-
Mapassocie une clé unique à une valeur. -
HashMapconvient dans la majorité des cas sans besoin d’ordre. -
LinkedHashMapconserve l’ordre d’insertion, tandis queTreeMaptrie les clés. -
ConcurrentHashMapest préférable lorsqu’une même structure est utilisée par plusieurs threads. -
equals()ethashCode()déterminent si deux clés sont considérées comme identiques.

Comprendre le fonctionnement d’une Map en Java
Une Map stocke des associations entre une clé et une valeur. La clé est unique, alors que plusieurs clés peuvent pointer vers la même valeur. Si j’ajoute une nouvelle valeur avec une clé déjà présente, Java remplace l’ancienne valeur au lieu de créer un doublon.
Map ages = new HashMap<>();
ages.put("Camille", 28);
ages.put("Nora", 34);
ages.put("Camille", 29);
System.out.println(ages.get("Camille")); // 29
Le type Map est une interface. Je recommande donc de déclarer la variable avec cette interface et de choisir l’implémentation uniquement au moment de l’instanciation. Cette habitude rend le code plus facile à faire évoluer si les besoins changent.
Map pays = new HashMap<>();
Les opérations essentielles sont put() pour ajouter ou modifier, get() pour lire, remove() pour supprimer et containsKey() pour vérifier l’existence d’une clé. Pour parcourir les associations, entrySet() est généralement le choix le plus clair et le plus efficace.
for (Map.Entry entree : pays.entrySet()) {
System.out.println(entree.getKey() + " : " + entree.getValue());
}
Choisir entre HashMap, LinkedHashMap et TreeMap
Ces trois classes répondent à des besoins différents. Dans un projet classique, je commence par HashMap, puis je change d’implémentation seulement si l’ordre ou le tri fait partie du besoin fonctionnel. Utiliser une structure triée sans raison ajoute une contrainte inutile.
| Implémentation | Ordre des éléments | Performance habituelle | Cas d’usage |
|---|---|---|---|
HashMap |
Non garanti | Accès moyen en O(1) | Recherche rapide par clé |
LinkedHashMap |
Insertion ou accès | Accès moyen en O(1) | Résultats prévisibles, cache simple |
TreeMap |
Clés triées | O(log n) | Intervalles, ordre naturel, navigation |
HashMap pour les recherches rapides
HashMap s’appuie sur une table de hachage. Elle offre généralement des recherches, insertions et suppressions rapides, à condition que les clés disposent d’un bon contrat entre equals() et hashCode(). L’ordre d’affichage ne doit jamais être utilisé comme une information fiable.
Cette classe accepte les clés et valeurs nulles, avec certaines limites pratiques. Pour une configuration, un index en mémoire ou un regroupement de données, c’est souvent le choix le plus simple et le plus efficace.
LinkedHashMap quand l’ordre compte
LinkedHashMap conserve par défaut l’ordre d’insertion. Cela est utile pour produire une sortie stable, respecter l’ordre d’un fichier importé ou construire un cache rudimentaire en mode accès récent. Le coût mémoire est un peu supérieur à celui d’une HashMap, car la structure maintient aussi des liens entre les entrées.
TreeMap pour exploiter le tri
TreeMap organise les clés selon leur ordre naturel ou selon un Comparator. Je la choisis lorsque le programme doit récupérer les clés avant ou après une valeur, parcourir une plage ou afficher systématiquement les éléments dans l’ordre.
Map priorites = new TreeMap<>();
priorites.put(3, "Normale");
priorites.put(1, "Urgente");
priorites.put(2, "Haute");
System.out.println(priorites); // {1=Urgente, 2=Haute, 3=Normale}
Le tri a un prix. Pour une simple lecture par identifiant, TreeMap est souvent surdimensionnée. Il faut également vérifier que les clés sont comparables entre elles ou fournir un comparateur cohérent.
Manipuler les valeurs sans introduire de bugs
La différence entre une clé absente et une clé présente avec une valeur null peut devenir importante. Un appel à get() renvoie null dans les deux cas avec une HashMap, ce qui rend containsKey() utile lorsque cette distinction compte.
if (ages.containsKey("Camille")) {
Integer age = ages.get("Camille");
System.out.println(age);
}
Pour fournir une valeur par défaut, getOrDefault() évite souvent un test verbeux. Pour créer une valeur uniquement si la clé est absente, putIfAbsent() est plus explicite qu’une succession de lectures et d’écritures.
int score = scores.getOrDefault("Nora", 0);
scores.putIfAbsent("Nora", 10);
Les méthodes de calcul sont pratiques
Les méthodes computeIfAbsent(), computeIfPresent() et merge() sont particulièrement utiles pour regrouper des données. Elles réduisent le risque d’oublier un cas limite et rendent l’intention visible directement dans le code.
Map> articlesParAuteur = new HashMap<>();
articlesParAuteur
.computeIfAbsent("Nora", cle -> new ArrayList<>())
.add("Guide Java");
Pour compter des occurrences, merge() est souvent très lisible.
Map occurrences = new HashMap<>();
for (String mot : mots) {
occurrences.merge(mot, 1, Integer::sum);
}
Je conseille aussi de parcourir les entrées avec entrySet() plutôt que de récupérer chaque clé puis de refaire un get(). La différence est modeste sur une petite collection, mais la première approche exprime mieux l’intention et évite une recherche supplémentaire.
Les clés, equals et hashCode décident du résultat
Une clé n’est pas seulement une valeur rangée dans une table. Pour les structures basées sur le hachage, Java utilise hashCode() pour trouver une zone probable, puis equals() pour vérifier l’égalité. Si ces deux méthodes sont incohérentes, la carte peut contenir des entrées qui semblent impossibles à retrouver.
Une règle simple s’applique toujours. Si deux objets sont égaux selon equals(), ils doivent retourner le même résultat avec hashCode(). Dans une classe métier utilisée comme clé, je préfère généralement des objets immuables, dont les champs déterminants ne changent plus après l’insertion.
record UtilisateurKey(String region, long id) {}
Map utilisateurs = new HashMap<>();
utilisateurs.put(new UtilisateurKey("FR", 42), "Compte actif");
String statut = utilisateurs.get(new UtilisateurKey("FR", 42));
Modifier une clé après son insertion est une erreur classique. Si un champ utilisé par hashCode() change, l’entrée reste physiquement dans l’ancienne position, mais une recherche avec la nouvelle valeur peut échouer. Les record sont intéressants dans ce scénario parce qu’ils fournissent automatiquement une égalité et un hachage fondés sur leurs composants.
Lire aussi : Héritage Java - Maîtrisez `extends` pour un code robuste
Ne pas confondre clé et valeur
Dans un dictionnaire de produits, le numéro de référence doit être la clé si l’application recherche principalement un produit à partir de ce numéro. Le nom ou le prix deviennent alors des valeurs. Inverser ce choix complique les recherches et peut provoquer des remplacements inattendus lorsque deux produits partagent la même valeur.
Une carte ne convient pas non plus lorsqu’une clé doit correspondre à plusieurs éléments indépendants. Dans ce cas, la valeur peut être une List ou un Set, comme dans Map. Il faut simplement accepter que l’on construit alors une relation entre une clé et une collection.
Gérer l’ordre, la nullité et l’immutabilité
Beaucoup de bugs viennent d’une attente implicite concernant l’ordre. Une HashMap peut sembler conserver l’ordre d’insertion pendant un test, puis produire une séquence différente après une modification, une autre version du JDK ou un changement de capacité interne. Si l’ordre est visible par l’utilisateur ou utilisé dans un export, il faut le déclarer avec LinkedHashMap ou TreeMap.
Pour une configuration qui ne doit plus être modifiée, les fabriques Map.of() et Map.copyOf() offrent une solution compacte. Elles produisent des cartes non modifiables et refusent les clés ou valeurs nulles. Ce comportement est volontaire et permet de détecter plus tôt une donnée invalide.
Map constantes = Map.of(
"environnement", "production",
"region", "eu-west"
);
Une vue non modifiable et une copie indépendante ne sont pas toujours la même chose. Une vue peut continuer à refléter les changements de la structure originale, tandis qu’une copie ne dépend plus de cette structure. Le choix dépend donc de la durée de vie des données et de la personne qui en contrôle la modification.
Dans les API publiques, je préfère exposer Map plutôt qu’une classe concrète. Cela limite le couplage et laisse la possibilité de changer l’implémentation interne sans modifier les appelants.
Utiliser une Map dans un programme multithread
HashMap n’est pas conçue pour coordonner plusieurs écritures concurrentes. Une lecture simultanée peut être acceptable dans certaines situations si la structure est déjà publiée de manière sûre et n’est plus modifiée, mais les mises à jour concurrentes exigent une vraie stratégie de synchronisation.
ConcurrentHashMap est adaptée lorsque plusieurs threads doivent lire et modifier la même structure. Elle fournit des opérations atomiques comme putIfAbsent(), compute() et merge(), et elle n’accepte ni clé null ni valeur null. Cette restriction évite de confondre une absence de résultat avec une valeur explicitement nulle.
ConcurrentMap visites = new ConcurrentHashMap<>();
visites.merge("accueil", 1L, Long::sum);
Il ne suffit toutefois pas de remplacer automatiquement HashMap par ConcurrentHashMap. Si une opération comprend plusieurs étapes qui doivent rester cohérentes, il faut utiliser les méthodes atomiques appropriées ou protéger l’ensemble avec un mécanisme adapté. La sécurité thread-safe ne rend pas toutes les séquences métier atomiques.
Pour un besoin très spécifique, ConcurrentSkipListMap peut fournir une structure triée et concurrente, mais elle est plus spécialisée. Dans la plupart des services, le choix raisonnable reste simple: structure immuable si possible, HashMap en local, ConcurrentHashMap pour l’état partagé.
Les réflexes qui rendent une Map fiable
- Déclarer les variables avec
Mapplutôt qu’avecHashMaplorsque l’implémentation n’a pas besoin d’être exposée. - Choisir
LinkedHashMapsi un ordre stable est attendu par l’utilisateur ou par un test. - Choisir
TreeMapuniquement lorsque le tri ou les recherches par intervalle sont réellement nécessaires. - Ne jamais modifier les champs utilisés par
equals()ethashCode()pendant qu’un objet sert de clé. - Utiliser
containsKey()lorsque la différence entre absence et valeurnullest importante. - Préférer
getOrDefault(),putIfAbsent()etmerge()aux séquences manuelles répétitives. - Ne pas partager une
HashMapmodifiable entre threads sans protection explicite.
Mon conseil le plus concret est de partir du besoin observable, pas du nom de la classe. Demandez-vous si l’application doit seulement retrouver une valeur, préserver l’ordre, trier les clés, accepter des valeurs nulles ou supporter des écritures concurrentes. Une fois cette réponse claire, le choix de la structure devient presque mécanique et le code gagne immédiatement en lisibilité.
Une carte bien choisie évite surtout les mauvaises surprises
Pour une recherche rapide par identifiant, HashMap reste le choix par défaut. Ajoutez LinkedHashMap lorsque l’ordre compte, passez à TreeMap lorsque le tri apporte une vraie fonctionnalité et utilisez ConcurrentHashMap seulement pour un état partagé entre threads.
La performance n’est qu’une partie du sujet. La fiabilité dépend tout autant de l’égalité des clés, de leur immutabilité, de la gestion des valeurs absentes et de la clarté de l’API. C’est cette combinaison qui transforme une simple association clé-valeur en structure solide pour une application Java.