Dans cet article (partie 5 de la série sur les collections), nous allons nous concentrer sur la famille Map du Framework Collections. Nous verrons ses principes (association clé/valeur), les principales implémentations (HashMap, LinkedHashMap, TreeMap, etc), leurs différences, pièges courants et bonnes pratiques d’utilisation.
- Introduction aux collections Java
- Les listes (List) en Java
- Les ensembles (Set) en Java
- Les files (Queue) et Deques en Java
- Les maps (Map) en Java (vous êtes ici)
- Utilisations avancées des collections (article à venir)
Qu’est‑ce qu’une Map ?
Map<K, V>
n’étend pas Collection
: c’est une famille à part qui gère des associations clé/valeur. Chaque clé est unique au sein de la map et pointe vers au plus une valeur.
Signature :
public interface Map<K, V> { /* … */ }
Principes :
- Unicité des clés (la notion d’égalité dépend de l’implémentation : equals/hashCode, ordre trié, identité…).
- Une clé peut être associée à null selon l’implémentation.
- La plupart des implémentations ne sont pas thread‑safe par défaut (sauf
ConcurrentHashMap
,Collections.synchronizedMap
, etc.).
Méthodes clés de Map
Méthode | Description |
---|---|
V put(K key, V value) | Ajoute/remplace la valeur de key. Retourne l’ancienne valeur ou null. |
V get(Object key) | Récupère la valeur associée, ou null si absente. |
V getOrDefault(Object key, V defaultValue) | Récupère la valeur, ou defaultValue si absente. |
boolean containsKey(Object key) | true si la clé est présente. |
boolean containsValue(Object value) | true si au moins une entrée a cette valeur. |
V remove(Object key) | Supprime la clé et retourne l’ancienne valeur. |
boolean remove(Object key, Object value) | Supprime seulement si la clé est associée à value. |
V replace(K key, V value) | Remplace seulement si présente. Retourne l’ancienne valeur ou null. |
boolean replace(K key, V oldVal, V newVal) | Remplace conditionnellement. |
void replaceAll(BiFunction<K,V,V> f) | Remplace chaque valeur par f.apply(k, v). |
V putIfAbsent(K key, V value) | Met la valeur seulement si absente (ou associée à null selon implémentation). |
V compute(K key, BiFunction<K,V,V> remap) | Recalcule la valeur à partir de l’ancienne. Supprime si remap retourne null. |
V computeIfAbsent(K key, Function<K,V> m) | Calcule et insère seulement si absente. |
V computeIfPresent(K key, BiFunction<K,V,V> m) | Recalcule seulement si présente. Supprime si m retourne null. |
V merge(K key, V value, BiFunction<V,V,V> f) | Si absente, put(value). Sinon, remplace par f.apply(old, value). null => suppression. |
Set |
Vue des clés. |
Collection |
Vue des valeurs. |
Set<Map.Entry<K,V» entrySet() | Vue des entrées (clé/valeur). |
Remarques :
- Les vues
keySet
,values
,entrySet
sont liées à la map : modifier la vue modifie la map (et inversement). - Les opérations de type
compute*
etmerge
sont très utiles pour éviter les if/put répétitifs et sont atomiques surConcurrentHashMap
.
Parcourir une Map
- Parcourir les paires clé/valeur (le plus courant) :
for (Map.Entry<String, Integer> e : scores.entrySet()) {
System.out.println(e.getKey() + " => " + e.getValue());
}
- Parcourir seulement les clés :
for (String k : scores.keySet()) {
Integer v = scores.get(k);
}
- Remplacer des valeurs en place :
scores.replaceAll((k, v) -> v == null ? 0 : v * 2);
- Compter les fréquences avec
merge
:
for (String mot : mots) {
freqs.merge(mot, 1, Integer::sum);
}
Principales implémentations
HashMap
- Structure : table de hachage (buckets + arbres rouges/noirs au‑delà d’un seuil de collisions depuis Java 8).
- Ordre d’itération non garanti et susceptible de changer.
- Opérations de base (
get
,put
,remove
) en O(1). - Accepte une clé null et des valeurs null (plusieurs).
- Paramètres importants : capacité initiale, facteur de charge (load factor, 0.75 par défaut).
Cas d’usage : choix par défaut pour une map non ordonnée.
LinkedHashMap
- Même base que HashMap + liste doublement chaînée pour l’ordre d’insertion ou d’accès.
- Ordre d’itération prévisible ; peut servir de LRU simple avec
accessOrder=true
etremoveEldestEntry
.
Exemple LRU 100 entrées :
Map<K, V> cache = new LinkedHashMap<>(16, 0.75f, true) {
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > 100;
}
};
TreeMap (SortedMap/NavigableMap)
- Structure : arbre rouge‑noir, tri selon l’ordre naturel ou un
Comparator
passé au constructeur. - Opérations en O(log n), itération triée.
- Ne supporte pas les clés null avec l’ordre naturel.
- API Navigable :
firstKey
,lastKey
,lowerEntry
,floorKey
,ceilingEntry
,subMap
,headMap
,tailMap
…
Cas d’usage : besoin de tri, de recherches par plage, de bornes.
Hashtable (legacy)
- Ancienne map synchronisée, à éviter au profit de
ConcurrentHashMap
ouCollections.synchronizedMap
. - N’accepte pas null (ni clé ni valeur).
ConcurrentHashMap
- Map concurrente haute performance.
- Pas de null (ni clé ni valeur) pour éviter des ambiguïtés (
get
ne peut pas retourner null pour « absent » vs « valeur null »). - Opérations atomiques utiles :
compute*
,merge
,putIfAbsent
. - Bonne évolutivité sous contention (segmentation interne + verrous fins/cas sans verrou selon opérations).
Cas d’usage : partage de données entre threads avec très peu de blocages.
WeakHashMap
- Les clés sont faiblement référencées : si une clé n’est plus référencée ailleurs, l’entrée peut être collectée par le GC.
- Typiquement utilisé pour des caches de métadonnées non essentiels.
IdentityHashMap
- L’égalité des clés est basée sur l’identité (
==
) et nonequals
. - Très spécialisé (frameworks, graphes d’objets, déduplication par identité).
EnumMap
- Map optimisée pour des clés d’un même type
enum
. - Très compacte, itération dans l’ordre de déclaration des constantes.
- N’accepte pas null.
Égalité, hashCode et clés correctes
- Pour les maps basées sur le hachage (
HashMap
,LinkedHashMap
), les clés doivent respecter le contratequals
/hashCode
: deux clés égales (equals==true) doivent avoir le même hashCode. - Clés immuables recommandées (String, Integer, UUID, objets valeur immuables). Évitez les clés mutables insérées puis modifiées, qui « perdront » leur case de hachage.
- Attention aux types numériques différents (Integer vs Long) :
equals
retournera false, même si la valeur apparente est la même.
Nulls, ordre et vues
HashMap
/LinkedHashMap
acceptent une clé null et des valeurs null.TreeMap
avec ordre naturel n’accepte pas de clé null.ConcurrentHashMap
n’accepte aucun null.- Les vues
keySet
,values
,entrySet
sont dynamiques : supprimer viaiterator.remove()
surentrySet
est le moyen sûr pour retirer en cours d’itération.
Complexités et performances
HashMap
:get
/put
/remove
≈ O(1) ; pires cas O(n) mais mitigés par l’arborisation des buckets en cas de nombreuses collisions.LinkedHashMap
: proche deHashMap
+ léger surcoût d’ordre.TreeMap
:get
/put
/remove
≈ O(log n) ; itération triée.ConcurrentHashMap
: très bonne scalabilité, opérations atomiques utiles.
Bonnes pratiques
- Choisir la bonne clé : immuable, avec des
equals
/hashCode
corrects. - Définir une capacité initiale si vous connaissez la taille cible pour limiter les réallocations.
- Préférer
getOrDefault
,computeIfAbsent
,merge
aux séquences if/contains/put fragiles. - Ne pas itérer avec
keySet
si vous avez besoin des valeurs : utilisezentrySet()
. - Pour un cache LRU simple,
LinkedHashMap
+accessOrder=true
+removeEldestEntry
. - En concurrence : privilégier
ConcurrentHashMap
. ÉvitezHashtable
.
Pièges courants
- Modifier une clé après insertion (mutable) : accès et suppression deviennent impossibles via la clé modifiée.
- Confondre
containsKey
etcontainsValue
(souvent, on veutcontainsKey
). - Supposer un ordre stable avec
HashMap
: non garanti, ne basez pas votre logique dessus. - Utiliser null avec
ConcurrentHashMap
: interdit. - Oublier que
entrySet
,keySet
,values
sont des vues : certaines opérations modifient directement la map.
Exemples
Comptage de mots avec merge
Map<String, Integer> freqs = new HashMap<>();
for (String mot : texte.split("\\W+")) {
if (mot.isEmpty()) continue;
freqs.merge(mot.toLowerCase(), 1, Integer::sum);
}
Index inversé (clé = lettre initiale)
Map<Character, List<String>> index = new HashMap<>();
for (String nom : noms) {
char k = Character.toUpperCase(nom.charAt(0));
index.computeIfAbsent(k, key -> new ArrayList<>()).add(nom);
}
TreeMap et recherches par plage
NavigableMap<Integer, String> m = new TreeMap<>();
m.put(10, "dix");
m.put(20, "vingt");
m.put(30, "trente");
System.out.println(m.subMap(10, true, 20, true)); // {10=dix, 20=vingt}
Map immuable avec Map.of / Map.ofEntries
// Map.of: jusqu'à 10 paires clé/valeur
Map<String, Integer> ages = Map.of(
"alice", 30,
"bob", 25
);
// ages est immuable: toute tentative de modification lève UnsupportedOperationException
// Map.ofEntries: pratique au‑delà de 10 entrées ou pour plus de lisibilité
Map<String, Integer> scores = Map.ofEntries(
Map.entry("A", 1),
Map.entry("B", 2),
Map.entry("C", 3)
);
// À partir d’une Map existante, créer une copie immuable:
Map<String, Integer> unmodifiable = Map.copyOf(scores);
Notes:
- Map.of / Map.ofEntries / Map.copyOf rejettent les clés ou valeurs null (NullPointerException).
- Map.of rejette les clés dupliquées (IllegalArgumentException).
- Les maps retournées sont non modifiables; si les valeurs sont mutables, elles peuvent toujours changer indépendamment.
Conclusion
Map
est la brique clé pour modéliser des associations clé/valeur en Java. En choisissant l’implémentation adaptée (rapidité vs ordre vs tri vs concurrence) et en appliquant de bonnes pratiques (clés immuables, API compute/merge), vous éviterez la plupart des pièges et écrirez un code plus clair et plus performant.
Pour aller plus loin dans la série :
- Introduction aux collections Java
- Les listes (List) en Java
- Les ensembles (Set) en Java
- Les files (Queue) et Deques en Java
- Les maps (Map) en Java (vous êtes ici)
- Utilisations avancées des collections (article à venir)