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 :
- Pourquoi ?
- 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 :
- 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
- 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.
Commentaires