List est-il une sous-classe de List? Pourquoi les génériques Java ne sont-ils pas implicitement polymorphes?


Je suis un peu confus sur la façon 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). 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 donc, soit l'un pourrait être transmis à cette méthode. De ne pas faire. Si je veux y parvenir comportement, je dois dire explicitement à la méthode d'accepter une liste de tout sous-ensemble d'animal en disant doSomething(List<? extends Animal> animals).

Je comprends que c'est le comportement de Java. Ma question est pourquoi? Pourquoi est-polymorphisme généralement implicite, mais quand il s'agit de génériques, il doit être spécifié?

Author: kevinarpe, 2010-04-30

16 answers

Non, un List<Dog> est pas un List<Animal>. Considérez ce que vous pouvez faire avec un List<Animal> - vous pouvez y ajouter n'importe quel 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 très chat confus.

Maintenant, vous ne peut pas ajouter un Cat pour un List<? extends Animal>, car vous ne savez pas, 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. Le l'inverse est vrai pour List<? super Animal> - dans ce cas, vous pouvez y 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>.

 776
Author: Jon Skeet, 2016-08-25 06:57:14

Ce que vous cherchez s'appelle covariante du type paramètres. Le problème est qu'ils ne sont pas sûrs dans le cas général, en particulier pour les listes mutables. Supposons que vous ayez un List<Dog>, et qu'il soit autorisé à fonctionner comme un 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 casse donc le système de type.

Il serait utile d'ajouter une syntaxe pour autoriser les paramètres de type à être spécifié comme covariant, ce qui évite le ? extends Foo dans les déclarations de méthode, mais cela ajoute une complexité supplémentaire.

 69
Author: Michael Ekstrand, 2018-02-10 08:33:57

Un List<Dog> n'est pas List<Animal>, c'est que, par exemple, vous pouvez insérer un Cat en 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 lorsque cela est possible; par exemple, la lecture à partir d'un List<Dog> est similaire à la lecture à partir d'un List<Animal> but mais pas à l'écriture.

Les Génériques dans le langage Java et la Section sur les génériques des tutoriels Java ont une très bonne explication approfondie de la raison pour laquelle certaines choses sont ou ne sont pas polymorphes ou autorisé avec des génériques.

 42
Author: Michael Aaron Safyan, 2015-04-18 19:21:29

Je dirais que tout l'intérêt des génériques est qu'ils ne le permettent pas. Considérons la situation avec les tableaux, qui autorisent ce type de covariance:

  Object[] objects = new String[10];
  objects[0] = Boolean.FALSE;

Ce code compile bien, mais génère une erreur d'exécution (java.lang.ArrayStoreException: java.lang.Boolean dans la deuxième ligne). Il n'est pas typesafe. Le but des génériques est d'ajouter la sécurité du type de compilation, sinon vous pourriez simplement vous en tenir à une classe simple sans génériques.

Maintenant, il y a des moments où vous devez être plus flexible et c'est ce que le ? super Class et ? extends Class sont pour. Le premier est quand vous devez insérer dans un type Collection (par exemple), et le dernier est pour quand vous devez lire à partir de lui, d'une manière sûre de type. Mais le seul moyen de faire les deux en même temps est d'avoir un type spécifique.

 32
Author: Yishai, 2017-08-15 17:33:49

Un point, je pense, devrait être ajouté à ce que autres réponses mentionner, c'est que tout

List<Dog> n'est-pas-un List<Animal> en Java

, Il est également vrai que

Une liste de chiens est - une liste d'animaux en anglais (bien, sous une interprétation raisonnable)

La façon dont fonctionne l'intuition de l'OP - ce qui est tout à fait valable bien sûr - est la dernière phrase. Cependant, si nous appliquons cette intuition, nous obtenons un langage qui est pas Java-esque dans son système de type: Supposons que notre langage permette d'ajouter un chat à notre liste de chiens. Ce que cela signifierait? 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.

Pour le dire d'une autre manière: List<Dog> en Java ne signifie pas "une liste de chiens" en anglais, cela signifie "une liste qui peut avoir des chiens, et rien d'autre".

Plus généralement, L'intuition d'OP se prête à un langage dans lequel les opérations sur les objets peuvent changer leur type, ou plutôt, le(s) type (s) d'un objet est une fonction (dynamique) de sa valeur.

 28
Author: einpoklum, 2017-09-29 14:47:54

Pour comprendre le problème, il est utile de faire une comparaison avec les tableaux.

List<Dog> est pas sous-classe de List<Animal>.
Mais Dog[] est sous-classe de Animal[].

Les Tableaux sont reifiable et covariants.
Reifiable signifie que leurs informations de type sont entièrement disponibles à l'exécution.
Par conséquent, les tableaux fournissent la sécurité du type d'exécution mais pas la sécurité du type de 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 pas fournir la sécurité du type d'exécution, mais ils fournissent la sécurité du type de compilation.
Dans le code ci-dessous, si les génériques étaient covariants, il sera possible de faire 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());
 5
Author: outdev, 2017-09-29 20:01:43

Les réponses données ici ne m'ont pas pleinement convaincu. Donc à la place, je fais un autre exemple.

public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
    consumer.accept(supplier.get());
}

Ça sonne bien, n'est-ce pas? Mais vous ne pouvez passer Consumers et Suppliers Animals. Si vous avez un Mammal consommateur, mais un Duck fournisseur, ils ne devrait pas s'adapter bien que les deux sont des animaux. Afin d'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());
}

Fait sûr 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 du Supplier et limitons qu'il puisse être mis dans le 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 l'intuition que les relations Life -> Animal -> Mammal -> Dog, Cat etc., on pourrait même mettre un Mammal en Life consommateur, mais pas un String en Life consommateur.

 4
Author: glglgl, 2015-02-14 21:26:06

La logique de base d'un tel comportement est que Generics suit un mécanisme d'effacement de type. 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 tel processus d'effacement. Donc, pour revenir à votre question...

Supposons donc qu'il existe une méthode comme donnée ci-dessous:

add(List<Animal>){
    //You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}

Maintenant, si java permet à l'appelant d'ajouter une liste de type Animal à cette méthode, vous pouvez ajouter une mauvaise chose dans la collection et au moment de l'exécution, elle s'exécutera également en raison de l'effacement de type. Alors que dans le cas des tableaux, vous obtiendrez une exception d'exécution pour de tels scénarios...

Donc essentiellement ce comportement est implémenté de sorte que l'on ne peut pas ajouter une mauvaise chose dans la collection. Maintenant, je crois que type erasure existe afin de donner une compatibilité avec java hérité sans génériques....

 4
Author: Hitesh, 2018-07-03 08:34:57

En fait, vous pouvez utiliser une interface pour réaliser 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
Author: Angel Koh, 2015-07-12 04:14:24

Si vous êtes sûr que les éléments de la liste sont des sous-classes de ce super type donné, vous pouvez lancer la liste en utilisant cette approche:

(List<Animal>) (List<?>) dogs

Ceci est utile lorsque vous voulez passer la liste dans un constructeur ou itérer dessus

 1
Author: sagits, 2016-01-28 21:11:04

Le réponse ainsi que d'autres réponses sont correctes. Je vais ajouter à ces réponses avec une solution qui je pense 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 est l'ajout à la collection. C'est là que les choses se décomposer. Même enlever est OK.

Dans la plupart des cas, nous pouvons utiliser Collection<? extends T> plutôt que Collection<T> et qui devrait être le premier choix. Cependant, je viens de trouver des cas où il est pas facile de le faire. C'est l'objet de débats quant à savoir si c'est toujours la meilleure chose à faire. Je présente ici une classe DownCastCollection qui peut convertir un Collection<? extends T> en un Collection<T> (nous pouvons définir des classes similaires pour List, Set, NavigableSet,..) pour être utilisé lors de l'utilisation de l'approche standard est très gênant. Voici un exemple de comment l'utiliser (nous pourrions également utiliser Collection<? extends Object> dans ce cas, mais je garde simple à illustrer en utilisant 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 le 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
Author: dan b, 2017-11-07 09:13:01

Le sous-typage est invariant pour le type paramétré. Même difficile, 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 est utilisé par les tableaux, donc le tableau type Dog[] est un sous-type de Animal[].

Le sous-typage invariant garantit que les contraintes de type appliquées par Java ne sont pas violées. Considérez 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 indiqué par @ Jon Skeet, ce code est illégal, car sinon cela violerait les contraintes de type en renvoyant un chat quand un chien s'y attendait.

Il est instructif de comparer ce qui précède au 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 . Un tableau porte son type au moment de l'exécution de cette façon JVM peut appliquer sécurité de type de sous-typage covariant.

Pour comprendre cela plus loin regardons 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");
    }
}

En utilisant la commande javap -c Demonstration , cela montre 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éthode 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 rompu la rétrocompatibilité.

En conclusion, la sécurité à l'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 type paramétré sont rien de plus que du sucre syntaxique.

 1
Author: Root G, 2018-05-03 14:00:22

Permet de prendre l'exemple de JavaSE tutoriel

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 comme 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 Java "architects" avait 2 options qui résolvent ce problème:

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

  2. Considérez le sous type comme étant son supertype et limitez à compilez la méthode " add " (donc dans la méthode drawAll, si une liste de cercles, sous-type de forme, serait transmise, le compilateur devrait détecter cela et vous restreindre avec une erreur de compilation).

Pour des raisons évidentes, qui a choisi la première voie.

 0
Author: aurelius, 2016-02-27 13:00:01

Nous devrions également prendre en considération la façon 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 distinctes des classes qui finissent par être "créé" par le compilateur lorsque nous spécifier les arguments génériques. Et ceci est une hiérarchie plate (en fait, en ce qui concerne List n'est pas du tout une hiérarchie).

Un autre argument pour lequel la covariance n'a pas de sens en cas de générique classes est le fait qu'à la base toutes les classes sont les mêmes - sont des instances List. La spécialisation d'un List en remplissant l'argument générique n'étend pas la classe, elle le fait juste fonctionner pour cet argument générique particulier.

 0
Author: Cristik, 2018-03-02 07:48:43

Le problème a été bien identifié. Mais il y a une solution; faire doSomething générique:

<T extends Animal> void doSomething<List<T> animals) {
}

Maintenant, vous pouvez appeler doSomething avec List ou List ou List.

 0
Author: gerardw, 2018-03-28 18:56:11

Une autre solution consiste à construire une nouvelle liste

List<Dog> dogs = new ArrayList<Dog>(); 
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());
 0
Author: ejaenv, 2018-07-20 14:12:13