web-dev-qa-db-fra.com

Est-ce que List <Chien> est une sous-classe de List <Animal>? Pourquoi les génériques Java ne sont-ils pas implicitement polymorphes?

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é?

672
froadie

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>.

853
Jon Skeet

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.

72
Michael Ekstrand

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.

42

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.

32
Yishai

Je pense qu’il faudrait ajouter un point à ce que autreréponses mentionne

List<Dog> n'est pas-un List<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. 

30
einpoklum

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());
8
outdev

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 Consumers et Suppliers pour Animals. 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.

4
glglgl

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 ....

4
Hitesh

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());
3
Angel Koh

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

1
sagits

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.

1
Root G

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();

}

}

1
dan b

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>.

0
gerardw

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.

0
Cristik

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:

  1. ne considérez pas qu'un sous-type est implicitement un supertype, et donnez une erreur de compilation, comme cela se produit maintenant

  2. 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. 

0
aurelius

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());
0
ejaenv