Polymorphisme des listes génériques

Avec Java 5 les développeurs ont vu pour leur plus grand bonheur arriver dans le langage les types génériques. On allait enfin pouvoir manipuler les List, Set et Map avec un typage fort.

Par exemple avec Java 4, lorsque l'on créait une list, on pouvait y mettre n'importe quel objet :

List menagerie = new ArrayList();
menagerie.put(new Animal());
menagerie.put(new Cat());
menagerie.put(new Dog());
//arg faut pas que je fasse ça
//mais le compilateur ne dit rien 
menagerie.put(new HumanBeing());

Bien sur les risques d'erreurs étaient très élevés car le développeur était le seul garant qu'on ne mettait pas n'importe quoi dans la liste. Sans parler du code de cast que l'on devait écrire pour récupérer les type adhoc :

Iterator it = menagerie.iterator();
//cast obligatoire même si je sais que ma 
//list ne contient que des animaux
Animal animal = (Animal) it.next();

Mais en Java 5 tous ces problèmes sont "résolus" avec les types génériques (je dis résolu entre guillemet vous allez bientôt voir pourquoi).

List menagerie<Animal> = new ArrayList<Animal>();
menagerie.put(new Animal());
menagerie.put(new Cat());
menagerie.put(new Dog());
 
//compile pas ....
menagerie.put(new HumanBeing());

Enfin plus de cast necessaires lorsqu' on parcourt la liste :

Iterator<Animal> it = menagerie.iterator();
//pas obligé de caster
Animal animal =  it.next();

Mais quid du polymorphisme des types génériques ?

C'est ici que les développeurs ne comprennent plus, et c'est compréhensible.

Supposons que je développe une methode

public void nourrit(List<Animal> menagerie);

J'aimerais pouvoir l'invoquer en lui passant une liste de chats par exemple

List<Cat> cats = new ArrayList<Cat>();
//compile pas ....
nourrit(cats);

Ce code ne compile pas, la méthode nourrit attend une liste d'animaux pas une liste de chats, même si le chat est un animal ... Difficile de comprendre pourquoi, le chat hérite de animal, donc tout ce que cette méthode fait avec des animaux elle doit pouvoir le faire avec des chats. Logique et pourtant non le compilateur refuse.

Deux questions viennent alors :

  1. Pourquoi ?
  2. Il y a-t-il une solution à ce problème ?

Répondons à la première : Pourquoi ?

Cette situation est d'autant plus troublante que le polymorphisme avec les tableaux est quant à lui possible :

//j'ai développé cette méthodes
public void nourrit(Animal[] menagerie);
 
//compile sans problème car Cat hérite de Animal
Cat[] cats = new Cat[15];
nourrit(cats);

Seulement vous pouvez aussi faire de grosses bêtises :
Supposons que je développe cette méthode qui met un chat en tête du tableau d'animaux qu'on lui passe

public void metUnChatAuDebut(Animal[] animals){
   animals[0] = new Cat();
}

Je peux tout à fait invoquer cette méthode de la façon suivante :

Dog[] dogs = {new Dog(), new Dog(), new Dog()};
//ça compile parfaitement
metUnChatAuDebut(dogs);
//aïe !!! J'ai mis un chat dans un tableau de chiens, pauvre minou !

Si vous exécutez ce code, vous déclencherez une ArrayStoreException. C'est le chat qui souffle : jamais il n'aurait pensé que l'ArrayStoreException lui serait un jour salutaire.

En clair à l'exécution la machine virtuelle sait que le tableau qui lui est passé est un tableau de Dog et qu'on ne peut y mettre que des Dog ou des classes héritant de Dog et si vous avez pu échapper à la vigilance du compilateur vous n'échapperez pas à celle de la machine virtuelle.

Mais pour les listes typées, il en va tout autrement. En effet, pour pouvoir maintenir le code qui a été écrit à l'époque où les listes n'étaient pas génériques, Sun a choisi de ne faire de la généricité qu'une affaire de compilation. En fait au niveau bytecode ce sont toujours des Objects qui sont manipulés, ajoutés, retirés.

Ce qui signifie que par rapport à l'exemple précédent, on pourrait aller un cran plus loin dans l'exécution et effectivement mettre un chat dans une liste de chiens, car pour la JVM on met finalement un Object dans une liste d'Objects et ça pour le chat ça risque d'être un moment très pénible.

C'est parce que cette sécurité supplémentaire n'existe pas que Sun a décidé d'interdire une utilisation aussi directe du polymorphisme :

public void ajouteUnChat(List<Animal> animals){
   animals.add(new Cat());
} 
 
List<Dog> dogs = new ArrayList<Dog>(); 
dogs.add(new Dog()); 
dogs.add(new Dog()); 
dogs.add(new Dog());
//ça ne compilera pas !!!
//et c'est peut être pas plus mal ...
ajouteUnChat(dogs);

Et ceci nous amène à la deuxième question.

Y a-t-il une solution à ce problème ?

Oui, mais à deux conditions :

  1. si vous promettez de ne faire que des opérations de lecture sur la liste alors vous pourrez passer une liste de Dog à une méthode attendant une liste d'Animal
  2. En respectant la syntaxe wildcard
public int compteLesAnimaux(List<? extends Animal> animals){
   //fait une opération qui n'ajoute pas d'élément à la liste 
   //ou ne réassigne pas un élément de la liste 
   //par exemple
   return animals.size() 
} 
 
List<Dog> dogs = new ArrayList<Dog>(); 
dogs.add(new Dog()); 
dogs.add(new Dog()); 
dogs.add(new Dog());
//ça compile !!
compteLesAnimaux(dogs);

Mais ce code ne compilera pas :

public void ajouteUnChat(List<? extends Animal> animals){
   //erreur de compilation 
   animals.add(new Cat());
} 
 
//ce qui suit ne présente pas beaucoup 
//d'intérêt puisque ce qui précède ne 
//compile pas
List<Dog> dogs = new ArrayList<Dog>(); 
dogs.add(new Dog()); 
dogs.add(new Dog()); 
dogs.add(new Dog());
ajouteUnChat(dogs);

Si la généricité avait été proposée dès le départ, Sun aurait certainement eu d'autres options. On peut dire que les choix qui ont été faits sont ceux du moindre mal, mais dès lors ils deviennent un peu difficile à appréhender.

Vous trouverez en pièce jointe un micro projet eclipse illustrant mes propos.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.