Interface Map en Java
Mappages clé-valeur en Java avec l'interface Map — put, get, remove, keySet, values, entrySet.
Ce chapitre couvre le contrat Map : ses sept méthodes principales, les trois vues qu'il expose pour l'itération, les méthodes par défaut Java 8 qui rendent le code de gestion des maps moderne et concis, les règles de gestion des valeurs null selon l'implémentation, et la façon dont les maps comparent l'égalité. À la fin, vous saurez quels idiomes utiliser et quelle implémentation standard convient à un cas donné.
Map<K, V> est l'autre moitié du framework de collections. Contrairement à l'interface Collection, elle n'étend pas Collection — c'est une hiérarchie séparée, car stocker des clés associées à des valeurs est une abstraction différente de stocker un ensemble d'éléments. En interne, la plupart des implémentations de Set sont simplement des Map où l'on ignore la valeur, donc Map est en quelque sorte la structure primaire et Set est la sœur plus simple.
Le contrat est court : chaque clé est associée à au plus une valeur, les clés forment un ensemble (pas de clés en double), et les valeurs sont une collection arbitraire (les valeurs dupliquées sont permises). Ce qui change entre les implémentations, c'est l'ordre d'itération, la gestion des null, les invariants d'ordre et la sécurité des threads — mais les sept méthodes principales ci-dessous se comportent de la même façon sur toutes.
Les sept méthodes principales
V put(K key, V value); // insert or overwrite; returns previous value or null
V get(Object key); // lookup; returns null if missing
V remove(Object key); // delete; returns previous value or null
boolean containsKey(Object k); // does the key exist (even if value is null)?
boolean containsValue(Object v); // O(n) scan of values
int size();
boolean isEmpty();Quelques subtilités à bien intégrer :
-
putretourne la valeur précédente pour cette clé, ounulls'il n'y avait pas de correspondance. C'est ainsi qu'on implémente les idiomes « insérer si absent » — sauf que ce n'est pas nécessaire, carputIfAbsentfait exactement cela et est plus clair. -
getretournantnullsignifie soit « la clé n'est pas présente » soit « la clé est présente mais sa valeur estnull». C'est une ambiguïté si votre map autorise les valeurs null ; utilisezcontainsKeypour lever l'ambiguïté, ou — mieux — utilisezgetOrDefaultpour fournir une valeur sentinelle :int count = counts.getOrDefault("java", 0); // 0 if absent
Les trois vues
Une Map n'est pas itérable directement. Pour itérer, il faut lui demander l'une des trois vues de son contenu :
Set<K> keys = map.keySet();
Collection<V> values = map.values();
Set<Map.Entry<K, V>> es = map.entrySet();Ces vues sont vivantes — elles reflètent les modifications apportées à la map sous-jacente, et les modifications effectuées via la vue se propagent en retour. Supprimer une entrée via entrySet() la supprime de la map ; itérer sur keySet() et appeler iterator.remove() supprime l'entrée. On ne peut pas faire add sur keySet ou values (pas de valeur ou de clé avec laquelle faire la paire), mais on peut vider ou supprimer.
L'itération utilise presque toujours entrySet() — obtenir les deux parties de chaque paire en une fois est moins coûteux qu'appeler get(k) pour chaque clé :
for (Map.Entry<String, Integer> e : counts.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}Ou, la forme lambda ajoutée en Java 8 :
counts.forEach((k, v) -> System.out.println(k + " -> " + v));Les méthodes par défaut Java 8 qui comptent vraiment
Java 8 a ajouté plusieurs méthodes Map qui prennent une fonction et se comportent de manière atomique. Elles transforment beaucoup de patrons en trois lignes en une seule ligne :
getOrDefault(k, def)—get(k)maisdefau lieu denull.putIfAbsent(k, v)—putuniquement si la clé est absente.computeIfAbsent(k, fn)— calcule atomiquement la valeur si absente, la stocke et la retourne. La pierre angulaire de « mémoïser cet appel coûteux » :Map<String, List<Order>> byUser = new HashMap<>(); byUser.computeIfAbsent(order.user(), u -> new ArrayList<>()).add(order);computeIfPresent(k, biFn)— recalcule uniquement si la clé existe déjà. Utile pour les compteurs qui doivent ignorer les clés inconnues.compute(k, biFn)— universel : passe la valeur actuelle (ou null), retourne la nouvelle. Supprime l'entrée si la fonction retourne null.merge(k, v, biFn)— combine une nouvelle valeur avec celle existante le cas échéant. Le compteur classique :for (String w : words) { counts.merge(w, 1, Integer::sum); // first time: stores 1; subsequent: adds }
Ce sont les opérations qui rendent la gestion moderne des maps en Java concise. Utilisez-les au lieu des paires get/put.
Clés null et valeurs null
Les règles dépendent de l'implémentation :
| Classe | clé null | valeur null |
|---|---|---|
HashMap | une autorisée | plusieurs autorisées |
LinkedHashMap | une autorisée | plusieurs autorisées |
TreeMap | non | plusieurs autorisées |
Hashtable | non | non |
ConcurrentHashMap | non | non |
Map.of(...) (immutable) | non | non |
La règle générale pour le nouveau code : ne pas stocker de null dans une map. Utilisez Optional, une valeur sentinelle, ou tout simplement ne pas insérer l'entrée. La fabrique Map.of l'impose pour vous.
Égalité entre implémentations
Deux maps sont equals si leurs entrySet() sont égaux — mêmes clés, mêmes valeurs, quel que soit l'ordre d'itération ou l'implémentation. Un HashMap et un TreeMap avec les mêmes paires clé-valeur sont égaux. C'est la même règle d'« égalité structurelle » que suit Set.
Les implémentations standard, en un coup d'œil
| Classe | Structure de base | Ordre d'itération | Utilisation |
|---|---|---|---|
HashMap | table de hachage | non spécifié | le choix par défaut |
LinkedHashMap | table de hachage + liste chaînée | ordre d'insertion ou d'accès | caches LRU, itération prévisible |
TreeMap | arbre rouge-noir | trié par clé | requêtes de plage sur les clés, sortie triée |
Hashtable | table de hachage, synchronisée | non spécifié | héritage ; rarement le bon choix |
ConcurrentHashMap | table de hachage striée | non spécifié | code multi-threadé |
EnumMap | indexé par tableau de bits | ordre d'enum | Map<MyEnum, V> |
Map.of(...) | immutable | non spécifié | petites maps fixes |
Les chapitres suivants couvrent en détail les choix courants : HashMap, LinkedHashMap et TreeMap. ConcurrentHashMap et EnumMap apparaissent dans des parties ultérieures.
Un exemple concret : compteurs, regroupement et les trois vues
Le programme ci-dessous illustre les idiomes modernes des maps — merge pour compter, computeIfAbsent pour regrouper, les trois vues, et la distinction entre getOrDefault et get.
Ce qu'il faut retenir de l'exécution :
merge(word, 1, Integer::sum)est le comptage de mots moderne et idiomatique. Pas deget/put/vérification de null nulle part.computeIfAbsentcrée la liste vide exactement une fois par clé — une façon propre de construire uneMap<K, List<V>>sans parsemerif (m.get(k) == null) m.put(k, new ArrayList<>())partout.- Les trois vues sont des fenêtres vivantes sur la même map ;
entrySet()est le moyen le moins coûteux d'itérer quand on a besoin des deux moitiés de chaque paire. getOrDefaultsupprime la raison la plus courante de vérifier les null. Utilisez-le chaque fois qu'il existe une valeur par défaut sensée.- Un
HashMapet unTreeMapavec les mêmes entrées sontequalsl'un à l'autre ; la seule chose qui change est l'ordre d'itération.
Et ensuite
L'implémentation par défaut — et celle que vous verrez dans 90 % du code Java — est basée sur une table de hachage. HashMap est le chapitre suivant ; nous y couvrirons le tableau de buckets, l'optimisation de treeification de Java 8, et ce qu'il faut faire lorsque vos clés sont vos propres classes.