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 ConcurrentHashMap qui 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 que HashMap<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

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.