Je suis un peu confus quant à la manière dont les génériques Java gèrent l'héritage/polymorphisme.
Supposons la hiérarchie suivante -
Animal (parent)
Chien - Chat (Enfants)
Supposons donc que j'ai une méthode doSomething(List<Animal> animals)
. Selon toutes les règles d'héritage et de polymorphisme, je suppose qu'un List<Dog>
est un List<Animal>
et un List<Cat>
est un List<Animal>
- et l'un ou l'autre peut être transmis à cette méthode. Pas si. Si je souhaite obtenir ce comportement, je dois indiquer explicitement à la méthode d'accepter une liste de toutes les sous-classes d'Animal en indiquant doSomething(List<? extends Animal> animals)
.
Je comprends que c'est le comportement de Java. Ma question est pourquoi? Pourquoi le polymorphisme est-il généralement implicite, mais quand il s'agit de génériques, il doit être spécifié?
Non, un List<Dog>
est pas un List<Animal>
. Considérez ce que vous pouvez faire avec un List<Animal>
- vous pouvez y ajouter tout animal ... y compris un chat. Maintenant, pouvez-vous logiquement ajouter un chat à une portée de chiots? Absolument pas.
// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?
Soudain, vous avez un chat très confus.
Maintenant, vous ne pouvez pas ajouter une Cat
à un List<? extends Animal>
parce que vous ne savez pas que c'est un List<Cat>
. Vous pouvez récupérer une valeur et savoir que ce sera un Animal
, mais vous ne pouvez pas ajouter d'animaux arbitraires. L'inverse est vrai pour List<? super Animal>
- dans ce cas, vous pouvez ajouter un Animal
en toute sécurité, mais vous ne savez rien de ce qui pourrait en être extrait, car il pourrait s'agir d'un List<Object>
.
Ce que vous recherchez s'appelle type covariant parameters. Cela signifie que si un type d'objet peut être remplacé par un autre dans une méthode (par exemple, Animal
peut être remplacé par Dog
), il en va de même pour les expressions utilisant ces objets (donc List<Animal>
pourrait être remplacé par List<Dog>
). Le problème est que la covariance n'est pas sans danger pour les listes mutables en général. Supposons que vous avez un List<Dog>
et qu'il est utilisé comme List<Animal>
. Que se passe-t-il lorsque vous essayez d'ajouter un chat à ce List<Animal>
qui est vraiment un List<Dog>
? Autoriser automatiquement les paramètres de type à être covariants interrompt le système de types.
Il serait utile d'ajouter une syntaxe permettant aux paramètres de type d'être spécifiés en tant que covariant, ce qui évite le ? extends Foo
dans les déclarations de méthode, mais cela ajoute une complexité supplémentaire.
La raison pour laquelle List<Dog>
n'est pas un List<Animal>
, c'est que, par exemple, vous pouvez insérer un Cat
dans un List<Animal>
, mais pas dans un List<Dog>
... vous pouvez utiliser des caractères génériques pour rendre les génériques plus extensibles dans la mesure du possible; Par exemple, lire à partir d'un List<Dog>
équivaut à lire à partir d'un List<Animal>
- sans écrire.
Les génériques du langage Java et la Section sur les génériques des didacticiels Java ont une très bonne explication détaillée de la raison pour laquelle certaines choses sont ou ne sont pas polymorphes ou autorisées avec des génériques.
Je dirais que l'intérêt de Generics est qu'il ne le permet pas. Considérez la situation des tableaux qui permettent ce type de covariance:
Object[] objects = new String[10];
objects[0] = Boolean.FALSE;
Ce code se compile bien, mais génère une erreur d’exécution (Java.lang.ArrayStoreException: Java.lang.Boolean
dans la deuxième ligne). Ce n'est pas typesafe. Le but des génériques est d’ajouter le type de sécurité lors de la compilation, sinon vous pourriez vous en tenir à une classe ordinaire sans génériques.
Il arrive maintenant que vous ayez besoin de plus de flexibilité et c’est à cela que servent ? super Class
et ? extends Class
. Le premier correspond au moment où vous devez insérer un type Collection
(par exemple), et le dernier correspond au moment où vous devez le lire, de manière sûre. Mais la seule façon de faire les deux en même temps est d’avoir un type spécifique.
Je pense qu’il faudrait ajouter un point à ce que autreréponses mentionne
List<Dog>
n'est pas-unList<Animal>
en Java
c'est aussi vrai que
Une liste de chiens est une liste d'animaux en anglais (enfin, sous une interprétation raisonnable)
Le fonctionnement de l'intuition du PO - qui est bien sûr tout à fait valable - est la dernière phrase. Cependant, si nous appliquons cette intuition, nous obtenons un langage qui n'est pas Java-esque dans son système de typage: Supposons que notre langage autorise l'ajout d'un chat à notre liste de chiens. Qu'est-ce que ça veut dire? Cela signifierait que la liste cesse d'être une liste de chiens et reste simplement une liste d'animaux. Et une liste de mammifères et une liste de quadrapeds.
En d'autres termes: un List<Dog>
en Java ne signifie pas "une liste de chiens" en anglais, mais "une liste pouvant contenir des chiens, et rien d'autre".
Plus généralement, l'intuition de OP se prête à un langage dans lequel les opérations sur les objets peuvent changer de type, ou plutôt, le type d'un objet est une fonction (dynamique) de sa valeur.
Pour comprendre le problème, il est utile de comparer les tableaux.
List<Dog>
est pas sous-classe de List<Animal>
.
MaisDog[]
est la sous-classe de Animal[]
.
Les tableaux sont reifiables et covariants.
Reifiable signifie que leurs informations de type sont entièrement disponibles au moment de l'exécution.
Par conséquent, les tableaux fournissent une sécurité de type à l'exécution mais pas de sécurité à la compilation.
// All compiles but throws ArrayStoreException at runtime at last line
Dog[] dogs = new Dog[10];
Animal[] animals = dogs; // compiles
animals[0] = new Cat(); // throws ArrayStoreException at runtime
C'est l'inverse pour les génériques:
Les génériques sont effacés et invariants.
Par conséquent, les génériques ne peuvent fournir une sécurité de type à l'exécution, mais ils offrent une sécurité de type à la compilation.
Dans le code ci-dessous, si les génériques étaient covariants, il sera possible de générer pollution en tas à la ligne 3.
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
animals.add(new Cat());
Les réponses données ici ne m'ont pas pleinement convaincu. Alors, au lieu de cela, je fais un autre exemple.
public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
consumer.accept(supplier.get());
}
semble bien, n'est-ce pas? Mais vous ne pouvez transmettre que Consumer
s et Supplier
s pour Animal
s. Si vous avez un consommateur Mammal
, mais un fournisseur Duck
, ils ne devraient pas correspondre, bien que les deux soient des animaux. Pour interdire cela, des restrictions supplémentaires ont été ajoutées.
Au lieu de ce qui précède, nous devons définir des relations entre les types que nous utilisons.
Par exemple.,
public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
s'assure que nous ne pouvons utiliser qu'un fournisseur qui nous fournit le bon type d'objet pour le consommateur.
OTOH, nous pourrions aussi bien faire
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
consumer.accept(supplier.get());
}
où nous allons dans l’autre sens: nous définissons le type de Supplier
et nous limitons son inclusion dans Consumer
.
Nous pouvons même faire
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
où, ayant les relations intuitives Life
-> Animal
-> Mammal
-> Dog
, Cat
etc., nous pourrions même mettre un Mammal
dans un Life
consommateur, mais pas un String
dans un consommateur Life
.
La logique de base de ce comportement est que Generics
suit un mécanisme de type effacement. Donc, au moment de l'exécution, vous n'avez aucun moyen d'identifier le type de collection
contrairement à arrays
où il n'y a pas de processus d'effacement de ce type. Revenons donc à votre question ...
Supposons donc qu’il existe une méthode donnée ci-dessous:
add(List<Animal>){
//You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}
Désormais, si Java autorise l'appelant à ajouter une liste de type Animal à cette méthode, vous risquez d'ajouter un élément incorrect à la collection. Au moment de l'exécution, il sera également exécuté en raison de l'effacement du type. Alors qu'en cas de tableaux, vous obtiendrez une exception d'exécution pour de tels scénarios ...
Ainsi, ce comportement est essentiellement mis en œuvre de manière à ce que personne ne puisse ajouter une mauvaise chose à la collection. Maintenant, je pense que l’effacement de type existe pour donner une compatibilité avec l’ancien Java sans génériques ....
En fait, vous pouvez utiliser une interface pour obtenir ce que vous voulez.
public interface Animal {
String getName();
String getVoice();
}
public class Dog implements Animal{
@Override
String getName(){return "Dog";}
@Override
String getVoice(){return "woof!";}
}
vous pouvez ensuite utiliser les collections en utilisant
List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());
Si vous êtes sûr que les éléments de la liste sont des sous-classes de ce super type, vous pouvez lancer la liste en utilisant cette approche:
(List<Animal>) (List<?>) dogs
Ceci est utile lorsque vous souhaitez passer la liste dans un constructeur ou effectuer une itération dessus
Le sous-typage est invariant pour les types paramétrés. Même si la classe Dog
est un sous-type de Animal
, le type paramétré List<Dog>
n'est pas un sous-type de List<Animal>
. En revanche, covariant sous-typage étant utilisé par les tableaux, le tableau Type Dog[]
est un sous-type de Animal[]
.
Le sous-typage invariant garantit que les contraintes de type imposées par Java ne sont pas violées. Considérons le code suivant donné par @Jon Skeet:
List<Dog> dogs = new ArrayList<Dog>(1);
List<Animal> animals = dogs;
animals.add(new Cat()); // compile-time error
Dog dog = dogs.get(0);
Comme l'a déclaré @Jon Skeet, ce code est illégal car il enfreindrait les contraintes de type en renvoyant un chat lorsqu'un chien l'attendait.
Il est instructif de comparer ce qui précède à un code analogue pour les tableaux.
Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];
Le code est légal. Cependant, lève une exception array store store . Un tableau porte son type au moment de l’exécution de cette manière JVM peut appliquer la sécurité Type.
Pour mieux comprendre cela, examinons le bytecode généré par javap
de la classe ci-dessous:
import Java.util.ArrayList;
import Java.util.List;
public class Demonstration {
public void normal() {
List normal = new ArrayList(1);
normal.add("lorem ipsum");
}
public void parameterized() {
List<String> parameterized = new ArrayList<>(1);
parameterized.add("lorem ipsum");
}
}
La commande javap -c Demonstration
affiche le bytecode Java suivant:
Compiled from "Demonstration.Java"
public class Demonstration {
public Demonstration();
Code:
0: aload_0
1: invokespecial #1 // Method Java/lang/Object."<init>":()V
4: return
public void normal();
Code:
0: new #2 // class Java/util/ArrayList
3: dup
4: iconst_1
5: invokespecial #3 // Method Java/util/ArrayList."<init>":(I)V
8: astore_1
9: aload_1
10: ldc #4 // String lorem ipsum
12: invokeinterface #5, 2 // InterfaceMethod Java/util/List.add:(Ljava/lang/Object;)Z
17: pop
18: return
public void parameterized();
Code:
0: new #2 // class Java/util/ArrayList
3: dup
4: iconst_1
5: invokespecial #3 // Method Java/util/ArrayList."<init>":(I)V
8: astore_1
9: aload_1
10: ldc #4 // String lorem ipsum
12: invokeinterface #5, 2 // InterfaceMethod Java/util/List.add:(Ljava/lang/Object;)Z
17: pop
18: return
}
Observez que le code traduit des corps de méthodes est identique. Le compilateur a remplacé chaque type paramétré par son erasure . Cette propriété est cruciale, ce qui signifie qu’elle n’a pas supprimé la compatibilité avec les versions antérieures.
En conclusion, la sécurité d'exécution n'est pas possible pour les types paramétrés, car le compilateur remplace chaque type paramétré par son effacement. Cela rend les types paramétrés ne sont rien de plus que du sucre syntaxique.
La réponse ainsi que d'autres réponses sont correctes. Je vais ajouter à ces réponses une solution qui, à mon avis, sera utile. Je pense que cela revient souvent dans la programmation. Une chose à noter est que pour les collections (listes, ensembles, etc.) le problème principal consiste à ajouter à la collection. C'est là que les choses se détériorent. Même enlever est OK.
Dans la plupart des cas, nous pouvons utiliser Collection<? extends T>
plutôt que Collection<T>
et ce devrait être le premier choix. Cependant, je trouve des cas où il n’est pas facile de le faire. On peut se demander si c'est toujours la meilleure chose à faire. Je présente ici une classe DownCastCollection qui peut convertir un Collection<? extends T>
en Collection<T>
(nous pouvons définir des classes similaires pour List, Set, NavigableSet, ..) à utiliser lorsque l’approche standard est très incommode. Vous trouverez ci-dessous un exemple d'utilisation (nous pourrions également utiliser Collection<? extends Object>
dans ce cas, mais je garde la simplicité pour illustrer l'utilisation de DownCastCollection.
/**Could use Collection<? extends Object> and that is the better choice.
* But I am doing this to illustrate how to use DownCastCollection. **/
public static void print(Collection<Object> col){
for(Object obj : col){
System.out.println(obj);
}
}
public static void main(String[] args){
ArrayList<String> list = new ArrayList<>();
list.addAll(Arrays.asList("a","b","c"));
print(new DownCastCollection<Object>(list));
}
Maintenant la classe:
import Java.util.AbstractCollection;
import Java.util.Collection;
import Java.util.Iterator;
import Java.util.NoSuchElementException;
public class DownCastCollection<E> extends AbstractCollection<E> implements Collection<E> {
private Collection<? extends E> delegate;
public DownCastCollection(Collection<? extends E> delegate) {
super();
this.delegate = delegate;
}
@Override
public int size() {
return delegate ==null ? 0 : delegate.size();
}
@Override
public boolean isEmpty() {
return delegate==null || delegate.isEmpty();
}
@Override
public boolean contains(Object o) {
if(isEmpty()) return false;
return delegate.contains(o);
}
private class MyIterator implements Iterator<E>{
Iterator<? extends E> delegateIterator;
protected MyIterator() {
super();
this.delegateIterator = delegate == null ? null :delegate.iterator();
}
@Override
public boolean hasNext() {
return delegateIterator != null && delegateIterator.hasNext();
}
@Override
public E next() {
if(!hasNext()) throw new NoSuchElementException("The iterator is empty");
return delegateIterator.next();
}
@Override
public void remove() {
delegateIterator.remove();
}
}
@Override
public Iterator<E> iterator() {
return new MyIterator();
}
@Override
public boolean add(E e) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object o) {
if(delegate == null) return false;
return delegate.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
if(delegate==null) return false;
return delegate.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(Collection<?> c) {
if(delegate == null) return false;
return delegate.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
if(delegate == null) return false;
return delegate.retainAll(c);
}
@Override
public void clear() {
if(delegate == null) return;
delegate.clear();
}
}
Le problème a été bien identifié. Mais il y a une solution. faire quelque chose générique:
<T extends Animal> void doSomething<List<T> animals) {
}
maintenant, vous pouvez appeler quelque chose avec Liste <Chien>, Liste <Cat> ou Liste <Animal>.
Nous devrions également prendre en considération la manière dont le compilateur menace les classes génériques: dans "instancie" un type différent chaque fois que nous remplissons les arguments génériques.
Ainsi, nous avons ListOfAnimal
, ListOfDog
, ListOfCat
, etc., qui sont des classes distinctes qui finissent par être "créées" par le compilateur lorsque nous spécifions les arguments génériques. Et ceci est une hiérarchie plate (en ce qui concerne List
n’est pas une hiérarchie du tout).
Un autre argument pour lequel la covariance n'a pas de sens dans le cas de classes génériques est le fait qu'à la base, toutes les classes sont identiques - sont des instances List
. La spécialisation d'une List
en remplissant l'argument générique ne prolonge pas la classe, elle la fait simplement fonctionner pour cet argument générique particulier.
Prenons l'exemple de JavaSE tutorial
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
private int x, y, radius;
public void draw(Canvas c) {
...
}
}
public class Rectangle extends Shape {
private int x, y, width, height;
public void draw(Canvas c) {
...
}
}
Alors pourquoi une liste de chiens (cercles) ne devrait pas être considérée implicitement une liste d'animaux (formes) est à cause de cette situation:
// drawAll method call
drawAll(circleList);
public void drawAll(List<Shape> shapes) {
shapes.add(new Rectangle());
}
Donc, les "architectes" Java avaient 2 options pour résoudre ce problème:
ne considérez pas qu'un sous-type est implicitement un supertype, et donnez une erreur de compilation, comme cela se produit maintenant
considérez le sous-type comme étant son supertype et restreignez la compilation avec la méthode "add" (ainsi dans la méthode drawAll, si une liste de cercles, un sous-type de forme, est transmise, le compilateur doit le détecter et limiter la compilation à une erreur de compilation. cette).
Pour des raisons évidentes, cela a choisi la première façon.
une autre solution est de construire une nouvelle liste
List<Dog> dogs = new ArrayList<Dog>();
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());