Tables associatives


Avant d'expliquer ce que sont les tables associatives, commencons par montrer à quoi elles servent, ou plus exactement, à quoi elles peuvent servir, à l'aide de deux exemple.

A quoi ca sert ?

Premier exemple: représentation d'un ensemble d'objets

Reprenons la classe Ville que nous avions déjà utilisée pour illustrer les listes génériques.:

 Ville {
  String Nom;
  String CodePostal;
}

Supposons que l'on souhaite représenter des ensembles de villes. Nous avons vu que la classe TreeSet de Java permet de résoudre ce problème. On pourrait donc déclarer un ensemble de ville E de la manière suivante:

 TreeSet < Ville > E ;

Ce serait une manière de procéder, mais pas la plus pratique.

Pour quelle raison ?

Et bien voilà: une ville est définie de manière unique par son code postal. Il serait donc intéressant de gérer un ensemble ville par les codes postaux. Par exemple:

  1. Retrouver le nom d'une ville de code postal donné.
  2. Supprimer une ville de code postal donné.
  3. Modifier le nom d'une ville de code postal donné.

Il est possible de faire ceci avec la classe TreeSet, mais ce n'est pas très pratique car on est obligé de parcourir les éléments de l'ensemble pour retrouver la ville associée à un code postal. C'est ici qu'interviennent les tables associatives représentables en Java par la classe générique TreeMap. En déclarant l'ensemble E comme ceci:

 TreeMap < String, Ville > E ;

nous pouvons par exemple:

  1. Retrouver le nom d'une ville de code postal "68150" par E.get("68150").
  2. Supprimer la ville de code postal "54000" via E.remove("54000").
  3. Modifier le nom de la ville de code postal "68260" en faisant E.put("68260" , le nouveau nom)

Quoi de plus pratique ?

La situation que nous venons de décrire se présentera chaque fois que l'on cherche à représenter un ensemble d'objets possédant un identifiant, c'est à dire un attribut particulier permettant d'identifier de manière unique chaque objet de la classe.

Dans la pratique, cette situation se rencontre fréquemment. Pensez par exemple à une voiture identifiée par son immatriculation, une personne identifiée par son numéro de sécurité sociale, etc ....

Dans la suite du cours, nous verrons que cette représentation des ensembles d'objets est particulièrement bien adaptée pour représenter des associations entre objets.


Deuxième exemple: le tableau généralisé

Supposons que l'on souhaite mémoriser la température moyenne pour chaque mois de l'année, les mois étant représentés par des chaines de caractères et les températures par des nombres. On pourrait imaginer un tableau de nombres indicé par les noms des mois. Malheureusement, l'indice d'un élément de tableau ne peut être qu'un nombre entier. Les tables associatives permettent de lever cette limitation. En effet, en déclarant une table associative de la manière suivante:

 TreeMap < String, Double > T ;

T.get("Juillet"), nous donnerait par exemple, la température moyenne du mois de Juillet.

On peut donc aussi voir une table associative comme une sorte de tableau dont les indices ne sont pas nécessairement des nombres entiers. Dans l'exemple précédent, l'appel de méthode T.get(Juillet") est assez similaire à T["Juillet"] retournant la valeur de l'élément d'indice "Juillet". Cette dernière notation est d'ailleurs utilisée dans d'autres langages utilisant les tables associatives (PHP, JavaScript par exemple).

Principes

Définition formelle
D'un point de vue mathématique une table associative représente une application (map ou mapping en anglais) entre deux ensembles: l'ensemble des clés et l'ensemble des valeurs.

Certaines clés peuvent être associées à aucune valeur et une même valeur peut être associée à plusieurs clés. Par contre, de par la définition mathématique d'une application, une clé ne peut être reliée qu'à une seule valeur.

Table-Asso-Formel.jpg, 58kB

On peut également définir une table associative comme un ensemble de paires (clé,valeur) . L'application ci-dessus serait par exemple l'ensemble de paire:

 { (a,1) (c;1) (d;4) (e;3) }

Déclaration en Java

En Java, une table associative T se déclare ainsi:

 TreeMap < K, V > T ;

Il s'agit donc d'une classe générique paramètrée par deux classes:

Limitation: dans ce cours nous supposerons que K est la classe String (ou la classe enveloppe d'un type primitif). Cette limitation n'est pas très gènante car dans la pratique les clés sont très souvent représentables par des chaines de caractères. Les raisons de cette limitation sont les mêmes que celles qui nous ont conduit à restreindre TreeSet (utilisation obligatoire d'interface, redéfinition obligatoire des méthodes equals et compareTo).


Instantiation

Pour créer une nouvelle table associative T (vide) dont les clés sont de type K et les valeurs de type V:

  T = new TreeMap < K, V > ( );

Methodes utiles

Parcours

Il n'y a pas de boucle pour chaque pour parcourir une table associative. Par contre, il est possible de parcourir les clés de la table, que l'on peut obtenir via la méthode keySet. Comme cette méthode retourne l'ensemble des clés sous la forme d'un objet de la classe TreeSet, on peut parcourir les éléments d'une table T à l'aide de la boucle for adaptée à cette classe générique:

for (K cle : T.keySet()) {
     valeur =  T.get (cle) ;
     Traitement de la valeur
}

Exemple

Exemple

L'exemple de projet présenté ici utilise une table associative pour représenter un ensemble de villes. Il se nomme EnsembleVille et vous le trouverez dans le dossier Exemple-Java-ProgObjet2. Voici l'interface graphique du projet.

EnsembleVilles.jpg, 32kB

La classe Ville est déclarée comme suit:

 public class Ville {
  String Nom;
  String CodePostal;
  Ville (String n, String CP) {Nom = n; CodePostal = CP;}
}

Elle possède donc un constructeur pour créer et initialiser une ville à partir de son nom et de son code postal.

L'ensemble des villes est contenu dans la variable E déclarée comme suit:

 TreeMap <String,Ville> E =  new TreeMap <String,Ville> ( );

Initialisation

La procédure Initialiser vide l'ensemble E en lui appliquant la méthode clear, puis y ajoute successivement quatre villes à l'aide de la méthode put:

void Initialiser () {
   
   E.clear();
   E.put("54000",new Ville("Nancy","54000"));
   E.put("67000",new Ville("Strasbourg","54000"));
   E.put("68260",new Ville("Kingersheim","68260"));
   E.put("68150",new Ville("Ribeauvillé","68150"));
  
} 

Affichage de l'ensemble

Dans la procédure AfficherEnsemble, nous parcourons la table associative E à l'aide d'une boucle for:

void AfficherEnsemble () {
    
     Ville v;
  
     es.Effacer(ZT_ListeVille);
    
     for (String cp : E.keySet()) {
       v = E.get(cp);
       es.Afficher(cp+"-"+v.Nom, ZT_EnsembleVille);   
       }
} 

Affichage d'une ville particulière

Le bouton Afficher permet d'afficher le nom d'une ville de code postal donné. Voici le code de la procédure évènementielle associée:

private void BT_AfficherActionPerformed(...) {                                            
  String cp = es.Lire(CT_CodePostal);
  Ville v = E.get(cp);
       
  if (v != null) {
          es.Afficher(v.Nom, CT_Nom);
       }
  else
         es.AfficherMessage("Ville inexistante!");
}  

Nous utilisons ici la méthode get pour obtenir la ville associée (sous la forme d'une référence à un objet de la classe Ville) au code postal. S'il n'y a pas de ville associée à ce code postal dans la table, le résultat retourné est null.


Adjonction d'une ville

Le bouton Ajouter ajoute une nouvelle ville donnée par son code postal et son nom:

private void BT_AjouterActionPerformed(...) {                                           
String nom = es.Lire (CT_Nom);
String cp = es.Lire(CT_CodePostal);
Ville v = E.get(cp);
        
  if (v==null) {
    E.put(cp,new Ville(nom,cp));
    AfficherEnsemble (); }
  else es.AfficherMessage("Cette ville existe déjà !");
}
   

Nous avons ici un exemple d'application de la méthode put pour ajouter une nouvelle paire (clé,valeur) dans la table.


Modification d'une ville

Pour modifier le nom d'une ville, l'utilisateur saisi son code postal ainsi que son nouveau nom, puis clique sur le bouton Modifier. La procédure évènementielle suivante est alors déclenchée:

private void BT_ModifierActionPerformed(...) {                                            
String nom = es.Lire (CT_Nom);
String cp = es.Lire(CT_CodePostal);
Ville v = E.get(cp);
        
  if (v != null) {
    E.put(cp,new Ville(nom,cp));
    AfficherEnsemble (); }
  else es.AfficherMessage("Cette ville n'existe pas !");  
}   

Nous avons ici un exemple d'application de la méthode put pour modifier une valeur de la table.


Suppression d'une ville
private void BT_SupprimerActionPerformed(...) {                                             
String cp = es.Lire(CT_CodePostal);
Ville v = E.get(cp);
    
  if (v != null) {
    E.remove(cp);
    AfficherEnsemble ();
  }
  else
  es.AfficherMessage("Ville inexistante");
}  

Implementation

Derrière les tables associatives se cache une représentation par une liste de triplet de pointeurs. Chaque triplet représente une paire (clé, valeur) de la table par trois pointeurs:

La figure ci-dessous illustre l'implementation de la table E du projet EnsembleVille vu dans l'exemple précédent, lorsqu'elle contient quatres paires (clé, valeur):

Implementation-Table-Asso.jpg, 89kB

Remarque importante: lorsque vous clonez une table associative, vous ne faite que créer une copie de la liste des triplets de pointeurs. Les objets représentants les clés et la valeurs ne sont pas dupliqués. Cela signifie donc que toute modification de ces objets sera repercutée sur le clone.