janv.
2009
Java Quiz #27
Que pensez-vous de ce code?
(Quiz proposé par Grégory Boissinot)
class Animal {} class Dog extends Animal {} class Main { public static void printAnimals(List<Animal> animals) { ... } public static void printAnimals(Animal[] animals) { ... } public static void main(String args[]) { Dog gromit = new Dog(); Dog[] dogArray = {gromit}; printAnimals(dogArray); List<Dog> dogList=new ArrayList<Dog>(); dogList.add(gromit); printAnimals(dogList); } }
Réponse :
Ce code ne compile pas.
Le compilateur indique que l'appel de la méthode printAnimals()
de la ligne 7 est illégal, puisqu'on lui passe un paramètre de type List<Dog>
, alors qu'elle attend un paramètre de type List<Animal>
.
Vous objecterez que "Dog extends Animal
" et que le même appel avec un tableau ne semble pas poser de problème. Voyons ces deux points.
Le cas des collections
Imaginons un instant que l'appel de la méthode printAnimals()
de la ligne 7 avec un paramètre de type List<Dog>
soit valide.
En imaginant également l'existence d'une classe "Cat extends Animal
", dans le corps de la méthode, nous pourrions écrire ceci :
public static void printAnimals(List<Animal> animals) { animals.add(new Cat()); }
Voyez-vous le problème ? Nous venons juste d'ajouter un Cat
dans une liste de Dog
, au mépris du typage de la collection ! Car, à l'intérieur de la méthode, le compilateur ne voit que le type déclaré du paramètre, à savoir List<Animal>
, et il estime parfaitement légal d'ajouter un Cat
dans une collection d'Animal
.
C'est pour éviter ce genre de situations que certaines restrictions s'appliquent aux méthodes prenant en paramètre des collections paramétrées.
Peut-on assouplir ces règles ? Dans une certaine mesure, c'est possible. Par exemple en déclarant :
public static void printAnimals(List<? extends Animal> animals) { ... }
Il est désormais possible d'appeler la méthode avec une List<Dog>
(puisque Dog extends Animal
). Dans la méthode, on pourra toujours parcourir la collection, mais pour éviter le problème évoqué plus haut, il sera alors interdit d'y ajouter de nouveaux éléments.
Par exemple, en modifiant ainsi le corps de la méthode :
public static void printAnimals(List<? extends Animal> animals) { // Lecture OK for (Animal a : animals) { System.out.println(a); } // Ecriture : problème ! animals.add(Dog()); animals.add(Animal()); }
Le compilateur indique qu'il est impossible d'ajouter de nouveaux éléments à la collection animals, même un Animal
!
Le cas des tableaux
Le compilateur est moins strict avec les tableaux, et il est parfaitement possible de passer un Dog
à une méthode réclamant un Animal
. Mais le même problème de compatibilité des types demeure : une plus grande liberté à la compilation ouvre la porte à des erreurs de type runtime, plus difficiles à diagnostiquer, et provoquant le plantage de l'application :
public static void printAnimals(Animal[] animals) { // Lecture : OK for (Animal a : animals) { System.out.println(a); } // Ajout d'un type différent : problème ! animals[0] = new Cat(); }
L'exécution de cet exemple provoque une erreur de type ArrayStoreException
:
Exception in thread "main" java.lang.ArrayStoreException: Cat at Main.printAnimals(Main.java:30) at Main.main(Main.java:37)
Commentaires
Je vois trois problèmes !
- si la classe Main n'est pas publique, je ne pense pas que le code soit exécutable (mais je ne pense pas que ce soit l'objet du Quiz)
- passer un Dog pour un Animal fonctionne, sauf que rien ne nous empêche d'ajouter un Animal qui n'est pas un Dog au tableau passé en paramètre. Et là, le problème n'apparaîtra qu'à l'exécution. Mais à mon avis l'objet du Quiz est encore ailleurs.
- List<Dog> pour List<Animal> ? Hmmm... sans avoir le flair d'un Dog, je sens que l'objet du Quiz est plutôt là !
La méthode "public static void printAnimals(List<Animal> animals)" devrait être "public static void printAnimals(List<T extends Animal> animals)" si je ne m'abuse ... et ensuite, travailler avec le type T et non le type Animal dans le corps de la méthode.