Map en Java : Guide Complet
Découvrez toutes les implémentations de l'interface Map en Java : HashMap, TreeMap, LinkedHashMap et plus encore
Qu'est-ce qu'une Map en Java ?
Une Map en Java est une structure de données qui stocke des éléments sous forme de paires clé-valeur (key-value). Aussi appelée dictionnaire ou tableau associatif dans d'autres langages de programmation, cette collection permet d'associer une valeur unique à chaque clé. Contrairement aux listes (List) ou aux ensembles (Set), chaque élément d'une Map est identifié par une clé unique qui permet d'accéder rapidement à la valeur associée.
L'interface java.util.Map fait partie du Java Collections Framework depuis Java 1.2
et constitue l'un des piliers de la programmation Java. Elle ne hérite pas de l'interface
Collection car son fonctionnement par paires clé-valeur est fondamentalement différent
des autres collections. En Java, une Map est paramétrée avec deux types génériques : le type de la clé
(K) et le type de la valeur (V), ce qui garantit la sécurité du typage à la compilation.
Les Maps sont omniprésentes dans le développement Java : gestion de caches, stockage de configurations, comptage d'occurrences, indexation de données, création de dictionnaires, mapping entre objets... Que vous développiez une application web avec Spring, une application Android, ou un microservice, vous utiliserez quotidiennement cette structure de données. Comprendre leurs différentes implémentations (HashMap, TreeMap, LinkedHashMap, ConcurrentHashMap) vous permettra de choisir la plus adaptée à chaque situation et d'optimiser les performances de votre code.
Pourquoi utiliser une Map en Java ?
Les Maps résolvent un problème fondamental en programmation : associer des données de manière efficace. Imaginez un annuaire téléphonique où vous recherchez un numéro à partir d'un nom, ou un dictionnaire où vous trouvez une définition à partir d'un mot. C'est exactement ce que permet une Map en Java.
Voici les principaux avantages des Maps :
- Accès rapide : Récupérer une valeur par sa clé en temps constant O(1) avec HashMap
- Unicité des clés : Chaque clé est unique, pas de doublons possibles
- Flexibilité : Stocker n'importe quel type d'objet comme clé ou valeur
- API riche : Nombreuses méthodes pour manipuler, filtrer et transformer les données
- Intégration Stream : Compatible avec l'API Stream de Java 8 pour le traitement fonctionnel
L'interface Map et ses méthodes principales
Avant d'explorer les différentes implémentations, voyons les méthodes essentielles que toute Map propose :
import java.util.Map;
import java.util.HashMap;
public class MethodesMap {
public static void main(String[] args) {
Map<String, Integer> ages = new HashMap<>();
// Ajouter des paires clé-valeur
ages.put("Alice", 25);
ages.put("Bob", 30);
ages.put("Charlie", 35);
// Récupérer une valeur par sa clé
Integer ageAlice = ages.get("Alice"); // 25
Integer ageInconnu = ages.get("David"); // null
// Récupérer avec valeur par défaut
Integer ageDavid = ages.getOrDefault("David", 0); // 0
// Vérifier l'existence d'une clé ou d'une valeur
boolean aAlice = ages.containsKey("Alice"); // true
boolean aAge30 = ages.containsValue(30); // true
// Supprimer une entrée
ages.remove("Charlie");
// Taille et vérification si vide
int taille = ages.size(); // 2
boolean estVide = ages.isEmpty(); // false
// Récupérer les clés, les valeurs ou les entrées
Set<String> cles = ages.keySet();
Collection<Integer> valeurs = ages.values();
Set<Map.Entry<String, Integer>> entrees = ages.entrySet();
// Parcourir avec forEach (Java 8+)
ages.forEach((nom, age) -> System.out.println(nom + " a " + age + " ans"));
// Vider complètement
ages.clear();
}
}
HashMap : l'implémentation la plus utilisée
HashMap est l'implémentation la plus courante et la plus performante pour la majorité des cas d'utilisation. Elle utilise une table de hachage pour stocker les éléments, offrant des opérations en temps constant O(1) en moyenne.
Caractéristiques de HashMap
- Performance : O(1) pour get, put, remove (cas moyen)
- Ordre : Aucune garantie sur l'ordre des éléments
- Null : Accepte une clé null et plusieurs valeurs null
- Thread-safety : Non synchronisée (non thread-safe)
import java.util.HashMap;
import java.util.Map;
public class ExempleHashMap {
public static void main(String[] args) {
// Création d'une HashMap
Map<String, Double> prix = new HashMap<>();
// Ajout d'éléments
prix.put("Pomme", 1.50);
prix.put("Banane", 0.99);
prix.put("Orange", 2.00);
prix.put("Fraise", 3.50);
// Mise à jour d'une valeur
prix.put("Pomme", 1.75); // Remplace l'ancienne valeur
// putIfAbsent : ajoute seulement si la clé n'existe pas
prix.putIfAbsent("Pomme", 1.99); // Ne change rien
prix.putIfAbsent("Kiwi", 2.50); // Ajoute Kiwi
// compute : calcul conditionnel
prix.compute("Banane", (fruit, prixActuel) ->
prixActuel != null ? prixActuel * 1.1 : 1.0
);
// merge : fusion de valeurs
prix.merge("Orange", 0.50, Double::sum); // Ajoute 0.50 au prix
// Affichage
System.out.println("Prix des fruits : " + prix);
}
}
Comment fonctionne le hachage ?
Le fonctionnement interne de HashMap repose sur le calcul d'un hash code
pour chaque clé. Ce hash détermine dans quel "bucket" (compartiment) l'élément sera stocké.
Pour que HashMap fonctionne correctement, les objets utilisés comme clés doivent implémenter
correctement hashCode() et equals().
public class Personne {
private String nom;
private int age;
public Personne(String nom, int age) {
this.nom = nom;
this.age = age;
}
// hashCode et equals doivent être cohérents
@Override
public int hashCode() {
return Objects.hash(nom, age);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Personne personne = (Personne) obj;
return age == personne.age && Objects.equals(nom, personne.nom);
}
}
// Utilisation comme clé
Map<Personne, String> emplois = new HashMap<>();
emplois.put(new Personne("Alice", 30), "Développeur");
emplois.put(new Personne("Bob", 25), "Designer");
LinkedHashMap : ordre d'insertion préservé
LinkedHashMap étend HashMap en maintenant une liste doublement chaînée entre les entrées. Cela garantit que l'itération se fait dans l'ordre d'insertion (ou d'accès si configuré ainsi).
Caractéristiques de LinkedHashMap
- Performance : Légèrement inférieure à HashMap (overhead de la liste chaînée)
- Ordre : Préserve l'ordre d'insertion par défaut
- Mode accès : Peut être configurée pour l'ordre d'accès (LRU)
- Null : Accepte une clé null et plusieurs valeurs null
import java.util.LinkedHashMap;
import java.util.Map;
public class ExempleLinkedHashMap {
public static void main(String[] args) {
// LinkedHashMap avec ordre d'insertion
Map<String, Integer> scores = new LinkedHashMap<>();
scores.put("Premier", 100);
scores.put("Deuxième", 85);
scores.put("Troisième", 70);
scores.put("Quatrième", 60);
// L'itération respecte l'ordre d'insertion
System.out.println("Classement :");
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue() + " pts");
}
// Affiche : Premier, Deuxième, Troisième, Quatrième (dans l'ordre)
}
}
Créer un cache LRU avec LinkedHashMap
LinkedHashMap peut être configurée en mode "access-order" pour implémenter facilement un cache LRU (Least Recently Used) :
import java.util.LinkedHashMap;
import java.util.Map;
public class CacheLRU<K, V> extends LinkedHashMap<K, V> {
private final int capaciteMax;
public CacheLRU(int capaciteMax) {
// true = access-order (ordre d'accès, pas d'insertion)
super(capaciteMax, 0.75f, true);
this.capaciteMax = capaciteMax;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// Supprime l'entrée la moins récemment utilisée si capacité dépassée
return size() > capaciteMax;
}
public static void main(String[] args) {
CacheLRU<String, String> cache = new CacheLRU<>(3);
cache.put("A", "Valeur A");
cache.put("B", "Valeur B");
cache.put("C", "Valeur C");
// Accès à A (le déplace en fin de liste)
cache.get("A");
// Ajout de D : B est supprimé (le moins récemment utilisé)
cache.put("D", "Valeur D");
System.out.println(cache.keySet()); // [C, A, D]
}
}
TreeMap : tri automatique des clés
TreeMap implémente l'interface NavigableMap et stocke
les éléments dans un arbre rouge-noir. Les clés sont automatiquement triées selon
leur ordre naturel ou un Comparator personnalisé.
Caractéristiques de TreeMap
- Performance : O(log n) pour get, put, remove
- Ordre : Clés triées selon ordre naturel ou Comparator
- Null : N'accepte pas de clé null (lève NullPointerException)
- Navigation : Méthodes avancées (firstKey, lastKey, subMap, etc.)
import java.util.TreeMap;
import java.util.NavigableMap;
public class ExempleTreeMap {
public static void main(String[] args) {
// TreeMap avec ordre naturel (alphabétique pour String)
NavigableMap<String, Integer> population = new TreeMap<>();
population.put("France", 67);
population.put("Allemagne", 83);
population.put("Espagne", 47);
population.put("Italie", 60);
population.put("Belgique", 11);
// Les clés sont automatiquement triées
System.out.println("Pays par ordre alphabétique :");
population.forEach((pays, pop) ->
System.out.println(pays + " : " + pop + "M habitants")
);
// Méthodes de navigation
System.out.println("Premier pays : " + population.firstKey()); // Allemagne
System.out.println("Dernier pays : " + population.lastKey()); // Italie
// Sous-ensemble de la carte
NavigableMap<String, Integer> sousEnsemble = population.subMap(
"Belgique", true, // inclus
"France", true // inclus
);
System.out.println("De Belgique à France : " + sousEnsemble);
// Clé inférieure/supérieure
System.out.println("Avant France : " + population.lowerKey("France")); // Espagne
System.out.println("Après France : " + population.higherKey("France")); // Italie
}
}
TreeMap avec Comparator personnalisé
import java.util.TreeMap;
import java.util.Comparator;
public class TreeMapComparator {
public static void main(String[] args) {
// TreeMap triée par ordre inverse (décroissant)
TreeMap<Integer, String> classement = new TreeMap<>(Comparator.reverseOrder());
classement.put(3, "Bronze");
classement.put(1, "Or");
classement.put(2, "Argent");
classement.forEach((place, medaille) ->
System.out.println(place + "e place : " + medaille)
);
// Affiche : 3e place, 2e place, 1e place
// TreeMap avec comparateur sur la longueur des clés
TreeMap<String, Integer> parLongueur = new TreeMap<>(
Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder())
);
parLongueur.put("Chat", 4);
parLongueur.put("Chien", 5);
parLongueur.put("Rat", 3);
System.out.println("Animaux par longueur de nom : " + parLongueur.keySet());
}
}
Hashtable : la version legacy thread-safe
Hashtable est l'ancêtre de HashMap, présent depuis Java 1.0. Elle est synchronisée par défaut mais cette synchronisation globale la rend moins performante que les alternatives modernes.
Caractéristiques de Hashtable
- Thread-safety : Synchronisée (toutes les méthodes)
- Performance : Moins bonne que ConcurrentHashMap en multi-thread
- Null : N'accepte ni clé null ni valeur null
- Usage : Préférez ConcurrentHashMap pour le code moderne
import java.util.Hashtable;
public class ExempleHashtable {
public static void main(String[] args) {
// Utilisation basique (déconseillée pour nouveau code)
Hashtable<String, String> config = new Hashtable<>();
config.put("host", "localhost");
config.put("port", "8080");
// config.put(null, "valeur"); // Lève NullPointerException !
// config.put("clé", null); // Lève NullPointerException !
String host = config.get("host");
System.out.println("Configuration : " + config);
}
}
Recommandation : Pour du nouveau code nécessitant une Map thread-safe, utilisez
ConcurrentHashMapqui offre de meilleures performances grâce à sa synchronisation plus fine.
ConcurrentHashMap : pour les environnements multi-thread
ConcurrentHashMap est la solution moderne pour les environnements concurrents. Elle utilise un mécanisme de verrouillage par segment (striped locking) permettant des accès simultanés sans bloquer toute la structure.
Caractéristiques de ConcurrentHashMap
- Thread-safety : Haute concurrence avec verrouillage fin
- Performance : Excellente en lecture, bonne en écriture
- Null : N'accepte ni clé null ni valeur null
- Atomicité : Opérations atomiques (putIfAbsent, compute, etc.)
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class ExempleConcurrentHashMap {
public static void main(String[] args) {
ConcurrentMap<String, Integer> compteurs = new ConcurrentHashMap<>();
// Opérations atomiques thread-safe
compteurs.putIfAbsent("visites", 0);
// Incrémentation atomique
compteurs.compute("visites", (cle, valeur) ->
valeur == null ? 1 : valeur + 1
);
// Ou avec merge
compteurs.merge("visites", 1, Integer::sum);
// Lecture sans verrouillage
int visites = compteurs.getOrDefault("visites", 0);
System.out.println("Nombre de visites : " + visites);
}
}
Compteur thread-safe avec ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class CompteurConcurrent {
private final ConcurrentHashMap<String, Long> compteurs = new ConcurrentHashMap<>();
public void incrementer(String cle) {
compteurs.merge(cle, 1L, Long::sum);
}
public long getCompteur(String cle) {
return compteurs.getOrDefault(cle, 0L);
}
public static void main(String[] args) throws InterruptedException {
CompteurConcurrent compteur = new CompteurConcurrent();
ExecutorService executor = Executors.newFixedThreadPool(10);
// 1000 incrémentations parallèles
for (int i = 0; i < 1000; i++) {
executor.submit(() -> compteur.incrementer("total"));
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("Total : " + compteur.getCompteur("total")); // 1000
}
}
EnumMap : optimisée pour les clés enum
EnumMap est une implémentation spécialisée extrêmement efficace lorsque les clés sont des constantes d'énumération. Elle utilise un tableau interne indexé par les ordinaux de l'enum.
Caractéristiques de EnumMap
- Performance : Très rapide, utilise un tableau interne
- Ordre : Ordre naturel de l'enum (ordre de déclaration)
- Mémoire : Compacte et efficace
- Null : Accepte les valeurs null mais pas les clés null
import java.util.EnumMap;
import java.util.Map;
public class ExempleEnumMap {
enum JourSemaine {
LUNDI, MARDI, MERCREDI, JEUDI, VENDREDI, SAMEDI, DIMANCHE
}
public static void main(String[] args) {
// EnumMap pour associer des horaires aux jours
Map<JourSemaine, String> horaires = new EnumMap<>(JourSemaine.class);
horaires.put(JourSemaine.LUNDI, "9h-18h");
horaires.put(JourSemaine.MARDI, "9h-18h");
horaires.put(JourSemaine.MERCREDI, "9h-12h");
horaires.put(JourSemaine.JEUDI, "9h-18h");
horaires.put(JourSemaine.VENDREDI, "9h-17h");
horaires.put(JourSemaine.SAMEDI, "10h-16h");
horaires.put(JourSemaine.DIMANCHE, "Fermé");
// Itération dans l'ordre de l'enum
horaires.forEach((jour, horaire) ->
System.out.println(jour + " : " + horaire)
);
// Vérification d'un horaire
if (horaires.get(JourSemaine.DIMANCHE).equals("Fermé")) {
System.out.println("Le magasin est fermé le dimanche");
}
}
}
WeakHashMap : gestion automatique de la mémoire
WeakHashMap utilise des références faibles (weak references) pour ses clés. Quand une clé n'est plus référencée ailleurs dans le programme, l'entrée correspondante peut être automatiquement supprimée par le garbage collector.
Caractéristiques de WeakHashMap
- Références : Clés avec références faibles
- GC : Entrées supprimées automatiquement si clé non référencée
- Usage : Caches, métadonnées associées à des objets
- Null : Accepte une clé null et plusieurs valeurs null
import java.util.WeakHashMap;
import java.util.Map;
public class ExempleWeakHashMap {
public static void main(String[] args) {
Map<Object, String> cache = new WeakHashMap<>();
// Création d'objets clés
Object cle1 = new Object();
Object cle2 = new Object();
cache.put(cle1, "Valeur 1");
cache.put(cle2, "Valeur 2");
System.out.println("Taille avant GC : " + cache.size()); // 2
// Suppression de la référence forte à cle1
cle1 = null;
// Suggestion au GC (ne garantit pas l'exécution immédiate)
System.gc();
// Attente pour laisser le GC travailler
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("Taille après GC : " + cache.size()); // Probablement 1
}
}
Cache de métadonnées avec WeakHashMap
import java.util.WeakHashMap;
import java.util.Map;
public class CacheMetadonnees {
// Cache qui ne retient pas les objets en mémoire
private static final Map<Object, Metadata> metadataCache = new WeakHashMap<>();
public static void associerMetadata(Object objet, Metadata metadata) {
metadataCache.put(objet, metadata);
}
public static Metadata getMetadata(Object objet) {
return metadataCache.get(objet);
}
static class Metadata {
String info;
long timestamp;
Metadata(String info) {
this.info = info;
this.timestamp = System.currentTimeMillis();
}
}
}
IdentityHashMap : comparaison par référence
IdentityHashMap est une implémentation particulière qui utilise
l'opérateur == au lieu de equals() pour comparer les clés.
Deux clés sont considérées égales uniquement si elles référencent le même objet.
Caractéristiques de IdentityHashMap
- Comparaison : Par référence (==) et non par equals()
- Hash : Utilise System.identityHashCode()
- Usage : Sérialisation, graphes d'objets, interning
- Null : Accepte une clé null et plusieurs valeurs null
import java.util.IdentityHashMap;
import java.util.HashMap;
import java.util.Map;
public class ExempleIdentityHashMap {
public static void main(String[] args) {
// Comparaison HashMap vs IdentityHashMap
Map<String, Integer> hashMap = new HashMap<>();
Map<String, Integer> identityMap = new IdentityHashMap<>();
String s1 = new String("test");
String s2 = new String("test");
// s1.equals(s2) est true, mais s1 == s2 est false
hashMap.put(s1, 1);
hashMap.put(s2, 2);
System.out.println("HashMap taille : " + hashMap.size()); // 1 (même clé)
identityMap.put(s1, 1);
identityMap.put(s2, 2);
System.out.println("IdentityHashMap taille : " + identityMap.size()); // 2 (clés différentes)
}
}
Comparatif des implémentations Map
Ce tableau résume les caractéristiques principales pour vous aider à choisir l'implémentation adaptée à votre besoin :
| Implémentation | Ordre | Performance | Thread-safe | Null clé |
|---|---|---|---|---|
HashMap |
Aucun | O(1) | Non | Oui |
LinkedHashMap |
Insertion/Accès | O(1) | Non | Oui |
TreeMap |
Trié | O(log n) | Non | Non |
Hashtable |
Aucun | O(1) | Oui | Non |
ConcurrentHashMap |
Aucun | O(1) | Oui (haute perf) | Non |
EnumMap |
Ordre enum | O(1) | Non | Non |
WeakHashMap |
Aucun | O(1) | Non | Oui |
IdentityHashMap |
Aucun | O(1) | Non | Oui |
Créer des Map immuables (Java 9+)
Depuis Java 9, vous pouvez créer des Maps immuables de manière concise avec
les méthodes factory Map.of() et Map.ofEntries() :
import java.util.Map;
import static java.util.Map.entry;
public class MapImmuables {
public static void main(String[] args) {
// Map.of() pour jusqu'à 10 paires clé-valeur
Map<String, Integer> notes = Map.of(
"Math", 18,
"Français", 15,
"Anglais", 16
);
// Map.ofEntries() pour plus de paires
Map<String, String> capitales = Map.ofEntries(
entry("France", "Paris"),
entry("Allemagne", "Berlin"),
entry("Italie", "Rome"),
entry("Espagne", "Madrid"),
entry("Portugal", "Lisbonne")
);
// Ces maps sont immuables
// notes.put("Histoire", 14); // Lève UnsupportedOperationException
System.out.println("Notes : " + notes);
System.out.println("Capitales : " + capitales);
}
}
Opérations courantes sur les Map
Parcourir une Map
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Charlie", 92);
// Méthode 1 : forEach avec lambda (Java 8+)
scores.forEach((nom, score) ->
System.out.println(nom + " : " + score)
);
// Méthode 2 : Itération sur entrySet
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
// Méthode 3 : Itération sur les clés
for (String nom : scores.keySet()) {
System.out.println(nom + " : " + scores.get(nom));
}
// Méthode 4 : Stream API
scores.entrySet().stream()
.filter(e -> e.getValue() > 90)
.forEach(e -> System.out.println(e.getKey() + " a excellé"));
Fusionner deux Maps
Map<String, Integer> map1 = new HashMap<>();
map1.put("A", 1);
map1.put("B", 2);
Map<String, Integer> map2 = new HashMap<>();
map2.put("B", 3);
map2.put("C", 4);
// Méthode 1 : putAll (écrase les valeurs existantes)
Map<String, Integer> fusion1 = new HashMap<>(map1);
fusion1.putAll(map2); // {A=1, B=3, C=4}
// Méthode 2 : merge avec fonction de résolution
Map<String, Integer> fusion2 = new HashMap<>(map1);
map2.forEach((cle, valeur) ->
fusion2.merge(cle, valeur, Integer::sum) // Additionne si conflit
);
// {A=1, B=5, C=4}
// Méthode 3 : Stream (Java 8+)
Map<String, Integer> fusion3 = Stream.concat(
map1.entrySet().stream(),
map2.entrySet().stream()
)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
Integer::sum // Fonction de merge en cas de conflit
));
Compter les occurrences
import java.util.HashMap;
import java.util.Map;
public class CompteurOccurrences {
public static void main(String[] args) {
String texte = "java map hashmap treemap linkedhashmap java collection java";
String[] mots = texte.split(" ");
Map<String, Integer> occurrences = new HashMap<>();
// Méthode classique
for (String mot : mots) {
occurrences.put(mot, occurrences.getOrDefault(mot, 0) + 1);
}
// Ou avec merge (plus élégant)
Map<String, Integer> occurrences2 = new HashMap<>();
for (String mot : mots) {
occurrences2.merge(mot, 1, Integer::sum);
}
// Ou avec Stream (Java 8+)
Map<String, Long> occurrences3 = Arrays.stream(mots)
.collect(Collectors.groupingBy(
mot -> mot,
Collectors.counting()
));
System.out.println("Occurrences : " + occurrences);
// {java=3, map=1, hashmap=1, treemap=1, linkedhashmap=1, collection=1}
}
}
Bonnes pratiques avec les Map Java
- Choisir la bonne implémentation : HashMap par défaut, TreeMap si tri nécessaire, ConcurrentHashMap en environnement multi-thread
-
Déclarer avec l'interface :
Map<K,V>plutôt queHashMap<K,V>pour la flexibilité - Implémenter hashCode/equals : Obligatoire pour les objets utilisés comme clés dans HashMap/LinkedHashMap
-
Spécifier la capacité initiale :
new HashMap<>(expectedSize)pour éviter les redimensionnements - Préférer getOrDefault : Plus sûr que get() qui peut retourner null
- Utiliser computeIfAbsent : Pour l'initialisation paresseuse de valeurs complexes
- Éviter Hashtable : Préférez ConcurrentHashMap pour le code concurrent moderne
// Exemple de bonnes pratiques
public class BonnesPratiquesMap {
// Déclarer avec l'interface Map
private final Map<String, List<String>> groupes = new HashMap<>();
public void ajouterAuGroupe(String groupe, String membre) {
// computeIfAbsent : crée la liste si elle n'existe pas
groupes.computeIfAbsent(groupe, k -> new ArrayList<>())
.add(membre);
}
public List<String> getMembres(String groupe) {
// getOrDefault pour éviter les NPE
return groupes.getOrDefault(groupe, Collections.emptyList());
}
}
Cas d'utilisation concrets
Cache simple avec expiration
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class CacheAvecExpiration<K, V> {
private final Map<K, CacheEntry<V>> cache = new ConcurrentHashMap<>();
private final long dureeVieMs;
public CacheAvecExpiration(long dureeVieMs) {
this.dureeVieMs = dureeVieMs;
}
public void put(K cle, V valeur) {
cache.put(cle, new CacheEntry<>(valeur, System.currentTimeMillis()));
}
public V get(K cle) {
CacheEntry<V> entry = cache.get(cle);
if (entry == null) return null;
if (System.currentTimeMillis() - entry.timestamp > dureeVieMs) {
cache.remove(cle);
return null;
}
return entry.valeur;
}
private static class CacheEntry<V> {
final V valeur;
final long timestamp;
CacheEntry(V valeur, long timestamp) {
this.valeur = valeur;
this.timestamp = timestamp;
}
}
}
Index inversé pour recherche textuelle
import java.util.*;
public class IndexInverse {
private final Map<String, Set<Integer>> index = new HashMap<>();
public void indexerDocument(int docId, String contenu) {
String[] mots = contenu.toLowerCase().split("\\W+");
for (String mot : mots) {
index.computeIfAbsent(mot, k -> new HashSet<>())
.add(docId);
}
}
public Set<Integer> rechercher(String mot) {
return index.getOrDefault(mot.toLowerCase(), Collections.emptySet());
}
public Set<Integer> rechercherTous(String... mots) {
Set<Integer> resultat = null;
for (String mot : mots) {
Set<Integer> docs = rechercher(mot);
if (resultat == null) {
resultat = new HashSet<>(docs);
} else {
resultat.retainAll(docs); // Intersection
}
}
return resultat != null ? resultat : Collections.emptySet();
}
}
Questions fréquentes sur les Map Java
Quelle est la différence entre HashMap et TreeMap ?
La principale différence entre HashMap et TreeMap concerne l'ordre et les performances. HashMap offre des performances optimales en temps constant O(1) pour les opérations get et put, mais ne garantit aucun ordre sur les éléments. TreeMap, en revanche, maintient automatiquement les clés triées selon leur ordre naturel ou un Comparator personnalisé, avec des performances en O(log n). Choisissez HashMap quand la rapidité est prioritaire, et TreeMap quand vous avez besoin d'un ordre déterministe sur les clés ou d'utiliser des méthodes de navigation comme subMap, headMap ou tailMap.
Comment synchroniser une HashMap en environnement multi-thread ?
Pour utiliser une Map dans un environnement concurrent avec plusieurs threads,
vous avez trois options principales. La meilleure approche pour du nouveau code Java
est d'utiliser ConcurrentHashMap qui offre d'excellentes performances
grâce à son verrouillage fin. Vous pouvez aussi utiliser
Collections.synchronizedMap(new HashMap<>()) pour wrapper une
HashMap existante, mais les performances seront moindres car la synchronisation
est globale. Évitez Hashtable qui est obsolète depuis Java 1.2.
Peut-on utiliser null comme clé dans une Map Java ?
La gestion des valeurs null varie selon l'implémentation choisie. HashMap, LinkedHashMap et WeakHashMap acceptent une clé null et plusieurs valeurs null, ce qui peut être pratique pour représenter des valeurs manquantes. En revanche, TreeMap, Hashtable, ConcurrentHashMap et EnumMap n'acceptent pas de clé null et lèvent une NullPointerException si vous tentez d'en insérer une. Cette restriction existe pour TreeMap car elle doit pouvoir comparer les clés, et pour ConcurrentHashMap pour éviter les ambiguïtés en environnement concurrent.
Comment itérer efficacement sur une Map en Java ?
La méthode la plus efficace pour parcourir une Map dépend de vos besoins.
Utilisez forEach() avec une expression lambda pour un code concis et moderne.
Si vous avez besoin d'accéder à la fois à la clé et à la valeur, itérez sur entrySet()
plutôt que sur keySet(). L'itération sur keySet suivie d'appels à get()
pour chaque clé est inefficace car elle double le nombre de recherches dans la table de hachage.
Pour des traitements plus complexes, combinez les Maps avec l'API Stream pour filtrer,
transformer et collecter les résultats de manière déclarative.
Quelle est la complexité des opérations sur les différentes Map ?
Les complexités algorithmiques varient selon l'implémentation. HashMap, LinkedHashMap, ConcurrentHashMap et EnumMap offrent des opérations en temps constant O(1) pour get, put et remove dans le cas moyen. TreeMap fonctionne avec une complexité logarithmique O(log n) car elle maintient un arbre rouge-noir équilibré. Cette différence devient significative pour de grandes collections : avec un million d'entrées, HashMap effectue une opération en une étape tandis que TreeMap peut nécessiter jusqu'à 20 comparaisons.
Comment convertir une Map en List ou en Set ?
La conversion entre collections est une opération courante en Java.
Pour obtenir une List des clés, utilisez new ArrayList<>(map.keySet()).
Pour une List des valeurs, utilisez new ArrayList<>(map.values()).
L'ensemble des entrées sous forme de Set est directement accessible via map.entrySet().
Avec l'API Stream, vous pouvez également transformer les données en les collectant
dans le type de collection souhaité.
Comment créer une Map avec des valeurs par défaut ?
Java ne propose pas nativement de Map avec valeurs par défaut comme certains autres langages,
mais vous pouvez obtenir ce comportement de plusieurs façons.
La méthode getOrDefault(key, defaultValue) retourne une valeur par défaut
si la clé est absente. Pour un comportement plus automatique, utilisez
computeIfAbsent(key, mappingFunction) qui crée et stocke la valeur
si la clé n'existe pas. Vous pouvez aussi étendre HashMap pour surcharger la méthode get.
Méthodes Java 8+ pour les Map
Java 8 a introduit de nouvelles méthodes puissantes dans l'interface Map qui simplifient considérablement le code et permettent un style de programmation plus fonctionnel. Ces méthodes sont particulièrement utiles pour manipuler les données de manière concise.
compute, computeIfAbsent et computeIfPresent
Map<String, Integer> compteur = new HashMap<>();
// computeIfAbsent : calcule la valeur si la clé est absente
// Idéal pour l'initialisation paresseuse
compteur.computeIfAbsent("visites", k -> 0);
// compute : calcule une nouvelle valeur (clé présente ou absente)
compteur.compute("visites", (cle, valeur) ->
valeur == null ? 1 : valeur + 1
);
// computeIfPresent : calcule seulement si la clé existe
compteur.computeIfPresent("visites", (cle, valeur) -> valeur * 2);
// Exemple pratique : grouper des éléments
Map<String, List<String>> groupes = new HashMap<>();
groupes.computeIfAbsent("fruits", k -> new ArrayList<>()).add("pomme");
groupes.computeIfAbsent("fruits", k -> new ArrayList<>()).add("banane");
// groupes = {fruits=[pomme, banane]}
getOrDefault et putIfAbsent
Map<String, Integer> config = new HashMap<>();
config.put("timeout", 30);
// getOrDefault : retourne une valeur par défaut si clé absente
int timeout = config.getOrDefault("timeout", 60); // 30
int maxRetries = config.getOrDefault("maxRetries", 3); // 3 (défaut)
// putIfAbsent : ajoute seulement si la clé n'existe pas
config.putIfAbsent("timeout", 60); // Ne change rien, clé existe
config.putIfAbsent("port", 8080); // Ajoute port=8080
replaceAll et forEach
Map<String, Double> prix = new HashMap<>();
prix.put("cafe", 2.50);
prix.put("the", 2.00);
prix.put("chocolat", 3.00);
// replaceAll : applique une fonction à toutes les valeurs
prix.replaceAll((produit, prixActuel) -> prixActuel * 1.1); // +10%
// forEach : parcourt toutes les entrées
prix.forEach((produit, prixProduit) ->
System.out.println(produit + " : " + prixProduit + "€")
);
Comment choisir la bonne implémentation Map ?
Le choix de l'implémentation Map dépend de vos besoins spécifiques. Voici un guide pour vous aider à faire le bon choix selon votre cas d'utilisation :
Utilisez HashMap quand :
- Vous avez besoin des meilleures performances possibles
- L'ordre des éléments n'a pas d'importance
- Vous travaillez dans un environnement mono-thread
- Vous avez besoin d'accepter des clés ou valeurs null
Utilisez LinkedHashMap quand :
- Vous devez préserver l'ordre d'insertion des éléments
- Vous voulez implémenter un cache LRU (Least Recently Used)
- L'itération doit se faire dans un ordre prévisible
Utilisez TreeMap quand :
- Vous avez besoin de clés automatiquement triées
- Vous utilisez des méthodes de navigation (firstKey, lastKey, subMap)
- Vous acceptez des performances légèrement inférieures O(log n)
Utilisez ConcurrentHashMap quand :
- Plusieurs threads accèdent à la Map en lecture/écriture
- Vous avez besoin d'opérations atomiques (compute, merge)
- Les performances en environnement concurrent sont critiques
Utilisez EnumMap quand :
- Toutes vos clés sont des constantes d'une énumération
- Vous voulez les meilleures performances possibles avec des enums
- L'ordre de l'énumération doit être respecté
Map et Stream API en Java
L'API Stream de Java 8 s'intègre parfaitement avec les Maps pour permettre des traitements de données expressifs et fonctionnels. Voici les opérations les plus courantes :
Map<String, Integer> notes = new HashMap<>();
notes.put("Alice", 18);
notes.put("Bob", 12);
notes.put("Charlie", 15);
notes.put("Diana", 20);
// Filtrer et collecter
Map<String, Integer> excellents = notes.entrySet().stream()
.filter(e -> e.getValue() >= 16)
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue
));
// {Alice=18, Diana=20}
// Trouver la clé avec la valeur maximale
String meilleurEleve = notes.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse("Aucun");
// Diana
// Calculer la moyenne des valeurs
double moyenne = notes.values().stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
// 16.25
// Transformer les valeurs
Map<String, String> mentions = notes.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue() >= 16 ? "Très Bien" :
e.getValue() >= 14 ? "Bien" : "Passable"
));
Ressources complémentaires
- Collections List - ArrayList et LinkedList en Java
- Collections Set - HashSet, TreeSet, LinkedHashSet
- Variables et types - Les bases du langage Java
- Classes et objets - Programmation orientée objet en Java
- Documentation officielle Oracle Map
Hiérarchie des interfaces Map en Java
Comprendre la hiérarchie des interfaces Map vous aide à choisir le bon type et à profiter des fonctionnalités spécifiques de chaque interface :
- Map<K,V> : Interface de base définissant le contrat fondamental pour toutes les implémentations. Fournit les méthodes get, put, remove, containsKey.
- SortedMap<K,V> : Étend Map pour les collections triées. Garantit un ordre sur les clés et ajoute firstKey, lastKey, subMap, headMap, tailMap.
- NavigableMap<K,V> : Étend SortedMap avec des méthodes de navigation plus riches : lowerKey, higherKey, floorKey, ceilingKey, descendingMap.
- ConcurrentMap<K,V> : Interface pour les Maps thread-safe avec opérations atomiques : putIfAbsent, remove conditionnel, replace.
- ConcurrentNavigableMap<K,V> : Combine ConcurrentMap et NavigableMap pour des collections triées et thread-safe (implémentée par ConcurrentSkipListMap).
Performances et optimisation des Map
Pour optimiser les performances de vos Maps, plusieurs bonnes pratiques s'appliquent. La capacité initiale et le facteur de charge (load factor) impactent directement les performances de HashMap et de ses dérivées.
Lorsque vous connaissez le nombre approximatif d'éléments à stocker,
spécifiez la capacité initiale pour éviter les redimensionnements coûteux :
new HashMap<>(expectedSize * 4 / 3 + 1).
Le facteur de charge par défaut (0.75) offre un bon compromis entre utilisation
de la mémoire et performances. Un facteur plus bas réduit les collisions
mais augmente la consommation mémoire.
Pour les clés de type String, sachez que Java optimise le calcul du hashCode en le mettant en cache. Pour vos propres classes utilisées comme clés, implémentez toujours hashCode de manière à distribuer uniformément les valeurs et à être cohérent avec equals. Un mauvais hashCode peut dégrader les performances de O(1) à O(n) si toutes les clés tombent dans le même bucket.
Conclusion
Les Map constituent l'une des structures de données les plus importantes et les plus utilisées en programmation Java. Cette collection basée sur des paires clé-valeur offre une flexibilité et des performances exceptionnelles pour associer des données. Dans ce guide complet, nous avons exploré en détail les différentes implémentations disponibles dans le Java Collections Framework.
HashMap reste le choix par défaut pour la majorité des cas d'utilisation grâce à ses performances optimales en O(1). Si vous avez besoin de préserver l'ordre d'insertion des éléments, optez pour LinkedHashMap. Pour des clés automatiquement triées et des opérations de navigation, TreeMap est la solution idéale. En environnement multi-thread où plusieurs threads accèdent concurremment aux données, ConcurrentHashMap offre la meilleure combinaison de sécurité et de performances.
N'oubliez pas les implémentations spécialisées : EnumMap pour des clés enum avec les meilleures performances possibles, WeakHashMap pour des caches qui ne retiennent pas les objets en mémoire, et IdentityHashMap pour les cas rares où vous devez comparer les clés par référence.
Maîtriser ces différentes implémentations et comprendre leurs caractéristiques vous permettra d'écrire du code Java plus performant, plus maintenable et mieux adapté à chaque situation. Les nouvelles méthodes introduites depuis Java 8 comme compute, merge et forEach simplifient considérablement la manipulation des données et permettent un style de programmation plus fonctionnel et expressif.