Programmation Objet en Java

Les Tables Associatives

Interface Map<K,V> & ses implémentations

📚 Cours magistral ☕ Java SE 8+ ✍️ François Bonneville
1

Qu'est-ce qu'une table associative ?

Une table associative (ou map, dictionnaire, tableau associatif) est une structure de données qui stocke des couples (clé → valeur). Chaque clé est unique et permet de retrouver sa valeur de manière efficace.

💡 Analogie
Un dictionnaire est une table associative : la clé est le mot, la valeur est sa définition. On ne peut pas avoir deux fois le même mot dans un dictionnaire, mais deux mots peuvent partager la même définition.

Exemples concrets

TableClé (K)Valeur (V)
Annuaire téléphoniqueNom & PrénomListe de numéros
Dictionnaire de langueMotDéfinition(s)
TraducteurMot françaisÉquivalents anglais
Cache HTTPURLRéponse en cache
Config applicativeClé de propriétéValeur de propriété

Les trois « vues » d'une table

Conceptuellement, toute table associative expose trois collections :

CollectionType JavaContrainte
Ensemble des clésSet<K>Aucun doublon
Collection des valeursCollection<V>Doublons autorisés
Ensemble des couplesSet<Map.Entry<K,V>>Aucun doublon de couple
⚠️ Attention
Deux clés ne peuvent pas être égales au sens de equals(). En revanche, une même valeur peut apparaître plusieurs fois associée à des clés différentes.
2

Hiérarchie de l'API Java Collections

Les tables associatives forment une branche distincte du Java Collections Framework : elles n'implémentent pas Collection<E> mais leur propre interface racine Map<K,V>.

«interface» Map<K,V>
hérite de
«interface» SortedMap<K,V>
hérite de
«interface» NavigableMap<K,V>
hérite de / implémente
AbstractMap<K,V>
classes concrètes
HashMap<K,V>
TreeMap<K,V>
3

Interface Map<K,V> – Méthodes essentielles

Les classes HashMap et TreeMap implémentent toutes deux l'interface Map<K,V>. Voici les méthodes fondamentales à connaître.

Déclaration simplifiée

public interface Map<K, V> {

    // ── Taille ─────────────────────────────────────────────
    boolean       isEmpty();
    int           size();

    // ── Ajout / Modification ────────────────────────────────
    V             put(K key, V value);   // retourne null ou ancienne valeur
    void          putAll(Map<? extends K, ? extends V> map);

    // ── Accès ───────────────────────────────────────────────
    V             get(Object key);         // null si clé absente
    V             getOrDefault(Object key, V defaultValue);

    // ── Suppression ─────────────────────────────────────────
    V             remove(Object key);      // null si clé absente
    void          clear();

    // ── Tests ───────────────────────────────────────────────
    boolean       containsKey(Object key);
    boolean       containsValue(Object value);

    // ── Vues ────────────────────────────────────────────────
    Set<K>                      keySet();
    Collection<V>               values();
    Set<Map.Entry<K,V>>         entrySet();
}

Détail des méthodes clés

MéthodeDescriptionCas particulier
put(k, v)Associe v à la clé kRetourne l'ancienne valeur ou null. Si la clé existait, la valeur est écrasée.
get(k)Retourne la valeur de la clé kRetourne null si la clé est absente
remove(k)Supprime le couple (k, v)Retourne l'ancienne valeur ou null
containsKey(k)Vérifie la présence d'une cléUtilise equals() pour la comparaison
getOrDefault(k, def)Retourne la valeur ou une valeur par défautPratique pour éviter les NullPointerException
✅ Bonne pratique Java 8+
Préférez map.getOrDefault(cle, valeurParDefaut) à un test if (map.containsKey(cle)) suivi de map.get(cle). C'est plus concis et thread-safe.
4

Interface SortedMap<K,V>

L'interface java.util.SortedMap<K,V> (depuis Java 1.2) étend Map<K,V> et définit les fonctionnalités d'une map dont les clés sont triées. L'ordre des clés est déterminé soit par l'implémentation de Comparable<K>, soit par un Comparator<K> fourni à la création.

⚠️ Cohérence equals / compareTo
La méthode equals() de la clé doit être cohérente avec compareTo() (ou le comparateur). Sinon, le comportement de la table est indéfini.

Méthodes propres à SortedMap

public interface SortedMap<K, V> extends Map<K, V> {

    K                   firstKey();              // plus petite clé
    K                   lastKey();               // plus grande clé

    SortedMap<K,V>      headMap(K toKey);         // clés < toKey
    SortedMap<K,V>      tailMap(K fromKey);       // clés >= fromKey
    SortedMap<K,V>      subMap(K fromKey, K toKey); // [fromKey, toKey[

    Comparator<? super K> comparator();          // null si ordre naturel
}
📌 Remarque sur les sous-tables
Les tables retournées par headMap(), tailMap() et subMap() sont des sous-tables actives de la table d'origine : toute modification dans la vue se répercute sur la table maître, et vice-versa.
6

Implémentations concrètes : HashMap vs TreeMap

HashMap<K,V> TreeMap<K,V>
Structure interneTable de hachageArbre rouge-noir (ABR équilibré)
Ordre des clésNon garantiOrdre naturel ou Comparator
Accès (get/put)O(1) amortiO(log n)
ItérationOrdre non prévisibleOrdre croissant des clés
Clé nulle1 clé null autoriséeInterdit (NullPointerException)
InterfacesMapMap, SortedMap, NavigableMap
Quand l'utiliser ?Performance maximale, ordre sans importanceQuand les clés doivent être triées

Constructeurs de TreeMap

// Constructeur 1 : ordre naturel (Comparable<K> doit être implémenté)
TreeMap<String, Integer> map1 = new TreeMap<>();

// Constructeur 2 : avec Comparator personnalisé
TreeMap<String, Integer> map2 = new TreeMap<>(
    Comparator.comparingInt(String::length)
              .thenComparing(Comparator.naturalOrder())
);

// Constructeur 3 : depuis une Map existante
HashMap<String, Integer> source = new HashMap<>();
// ... remplissage ...
TreeMap<String, Integer> map3 = new TreeMap<>(source);

Constructeurs de HashMap

// Constructeur 1 : par défaut (capacité 16, facteur de charge 0.75)
HashMap<String, Integer> map = new HashMap<>();

// Constructeur 2 : capacité initiale personnalisée
HashMap<String, Integer> map2 = new HashMap<>(100);
✅ Conseil
Par défaut, préférez HashMap pour la performance. Utilisez TreeMap uniquement si vous avez besoin que les clés soient triées ou si vous utilisez des méthodes de SortedMap/NavigableMap.
7

Parcours et vues d'une table

Une table associative ne dispose pas d'itérateur direct. On la parcourt via l'une de ses trois vues. Attention : toute modification effectuée via une vue (notamment remove()) est répercutée sur la table maître. En revanche, les ajouts ne sont pas autorisés par les vues.

Vue 1 – Ensemble des couples : entrySet()

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob",   87);
scores.put("Carol", 91);

// ── Java 5+ : for-each sur entrySet ─────────────────────
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + " → " + entry.getValue());
}

// ── Java 8+ : forEach avec lambda ───────────────────────
scores.forEach((nom, note) ->
    System.out.println(nom + " → " + note)
);

Vue 2 – Ensemble des clés : keySet()

Set<String> cles = scores.keySet();
for (String nom : cles) {
    System.out.println(nom);
}
// Sur un TreeMap, les clés sont affichées dans l'ordre naturel

Vue 3 – Collection des valeurs : values()

Collection<Integer> valeurs = scores.values();
// Collection (pas Set) car les valeurs peuvent contenir des doublons

int somme = valeurs.stream()
                   .mapToInt(Integer::intValue)
                   .sum();
System.out.println("Somme des notes : " + somme);
⚠️ Modification pendant l'itération
Il est interdit de modifier la taille d'une table pendant son parcours via une vue (sauf via iterator.remove()). Toute autre modification lève une ConcurrentModificationException.
8

Exemple complet : annuaire téléphonique

Nous allons créer un annuaire qui associe chaque Personne à une liste de numéros de téléphone (ListNo). L'annuaire doit afficher les personnes par ordre alphabétique sur le nom et le prénom, ce qui nous oriente vers un TreeMap.

Classe Personne (avec Comparable)

public class Personne implements Comparable<Personne> {

    private final String nom;
    private final String prenom;

    public Personne(String nom, String prenom) {
        this.nom    = nom;
        this.prenom = prenom;
    }

    // Getters omis pour la lisibilité

    /** Ordre alphabétique : nom puis prénom */
    @Override
    public int compareTo(Personne autre) {
        int cmp = this.nom.compareTo(autre.nom);
        return (cmp != 0) ? cmp : this.prenom.compareTo(autre.prenom);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Personne)) return false;
        Personne autre = (Personne) obj;
        return nom.equals(autre.nom) && prenom.equals(autre.prenom);
    }

    @Override
    public int hashCode() {
        return Objects.hash(nom, prenom);
    }

    @Override
    public String toString() {
        return nom + " " + prenom;
    }
}

Classe ListNo

public class ListNo {
    private final List<String> numeros = new ArrayList<>();

    public void ajouter(String numero) { numeros.add(numero); }

    @Override
    public String toString() { return numeros.toString(); }
}

Classe TableTelPersonne

import java.util.*;

public class TableTelPersonne {

    private final TreeMap<Personne, ListNo> annuaire;

    /** Constructeur : crée un annuaire vide */
    public TableTelPersonne() {
        annuaire = new TreeMap<>();
    }

    /** Ajoute un numéro pour une personne (crée l'entrée si besoin) */
    public void ajouterNumero(Personne p, String numero) {
        ListNo liste = annuaire.getOrDefault(p, null);
        if (liste == null) {
            liste = new ListNo();
            annuaire.put(p, liste);
        }
        liste.ajouter(numero);
    }

    /** Affiche toutes les personnes par ordre alphabétique */
    public void afficherPersonnes() {
        Set<Personne> cles = annuaire.keySet();
        System.out.println(cles);
    }

    /** Parcourt et affiche chaque couple (personne → numéros) */
    public void afficherAnnuaire() {
        for (Map.Entry<Personne, ListNo> e : annuaire.entrySet()) {
            System.out.println(e.getKey() + " : " + e.getValue());
        }
    }

    /** Recherche les numéros d'une personne */
    public ListNo rechercherNumeros(Personne p) {
        return annuaire.get(p);
    }
}

// ── Utilisation ────────────────────────────────────────────
TableTelPersonne a = new TableTelPersonne();

Personne alice = new Personne("Dupont", "Alice");
Personne bob   = new Personne("Martin", "Bob");

a.ajouterNumero(alice, "06 12 34 56 78");
a.ajouterNumero(alice, "01 23 45 67 89");
a.ajouterNumero(bob,   "07 98 76 54 32");

a.afficherAnnuaire();
// Dupont Alice : [06 12 34 56 78, 01 23 45 67 89]
// Martin Bob   : [07 98 76 54 32]
9

Bonnes pratiques

1 – Déclarer via l'interface

// ✅ Bon : dépendance à l'interface, pas à l'implémentation
Map<String, Integer> map = new HashMap<>();

// ❌ À éviter : couplage fort sur l'implémentation
HashMap<String, Integer> map = new HashMap<>();

2 – Implémenter equals() ET hashCode() pour les clés d'un HashMap

Si vous utilisez vos propres objets comme clés d'un HashMap, vous devez implémenter equals() et hashCode() de manière cohérente. Si a.equals(b) est vrai, alors a.hashCode() == b.hashCode() doit l'être également.

3 – Utiliser compute et merge (Java 8+)

Map<String, Integer> compteur = new HashMap<>();

// Incrémenter un compteur : classique
compteur.put("mot", compteur.getOrDefault("mot", 0) + 1);

// Idem avec merge (Java 8) : plus élégant
compteur.merge("mot", 1, Integer::sum);

4 – Récapitulatif des méthodes Java 8+

MéthodeDescription
forEach(BiConsumer)Itération fonctionnelle sur tous les couples
getOrDefault(k, def)Retourne la valeur ou une valeur par défaut
putIfAbsent(k, v)Ajoute le couple uniquement si la clé est absente
replace(k, v)Remplace la valeur uniquement si la clé est présente
merge(k, v, fn)Fusionne l'ancienne et la nouvelle valeur via une fonction
computeIfAbsent(k, fn)Calcule et ajoute la valeur si la clé est absente
computeIfPresent(k, fn)Recalcule la valeur si la clé est présente
✅ Pour aller plus loin
Explorez LinkedHashMap<K,V> si vous souhaitez conserver l'ordre d'insertion des entrées, et EnumMap<K,V> pour des performances optimales lorsque les clés sont des valeurs d'une enum.
🚫 Points d'attention
  • Ne jamais modifier les champs utilisés dans hashCode()/equals() d'une clé après son insertion dans un HashMap.
  • Ne jamais utiliser une clé null dans un TreeMap (lève une NullPointerException).