Question Est-ce que List est 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 / le polymorphisme.

Supposons la hiérarchie suivante -

Animal (Parent)

Chien - Chat (Enfants)

Alors supposons que j'ai une méthode doSomething(List<Animal> animals). Par toutes les règles de l'héritage et du polymorphisme, je suppose qu'un List<Dog>  est une List<Animal> et un List<Cat>  est une List<Animal> - et ainsi l'un ou l'autre pourrait être passé à cette méthode. Pas si. Si je veux réaliser ce comportement, je dois dire explicitement à la méthode d'accepter une liste de n'importe quel 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 le polymorphisme est-il généralement implicite, mais quand il s'agit de génériques, cela doit-il être spécifié?


616
2018-04-30 14:39


origine


Réponses:


Non, un List<Dog> est ne pas une List<Animal>. Considérez ce que vous pouvez faire avec un List<Animal> - vous pouvez ajouter tout animal à elle ... 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, c'est à votre tour ne peut pas ajouter un Cat à un List<? extends Animal> parce que 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. 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 être récupéré, car il pourrait être un List<Object>.


756
2018-04-30 14:44



Ce que vous cherchez s'appelle type covariant paramètres. Le problème est qu'ils ne sont pas sûrs dans le cas général, en particulier pour les listes modifiables. Supposons que vous avez un List<Dog>, et il est permis de fonctionner comme List<Animal>. Que se passe-t-il lorsque vous essayez d'ajouter un chat à ce List<Animal> ce qui est vraiment un List<Dog>? Le fait d'autoriser automatiquement les paramètres de type à covariant casse donc le système de types.

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


67
2018-04-30 14:44



La raison d'une List<Dog> n'est pas un List<Animal>, est-ce, par exemple, vous pouvez insérer un Cat dans une 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, lire d'un List<Dog> est similaire à la lecture d'un List<Animal> - mais pas d'écriture.

le Génériques dans la langue Java et le Section sur les génériques des tutoriels Java avoir une très bonne explication en profondeur de la raison pour laquelle certaines choses sont ou ne sont pas polymorphes ou permises avec des génériques.


42
2018-04-30 14:46



Je dirais que le but des Generics est de ne pas autoriser cela. Considérons la situation avec des tableaux, qui permettent 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). Ce n'est pas sûr. Le but de Generics est d'ajouter la sécurité du type de temps de compilation, sinon vous pourriez simplement coller avec une classe ordinaire 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 avez besoin d'insérer dans un type Collection (par exemple), et ce dernier est pour quand vous avez besoin de lire, d'une manière sûre de type. Mais la seule façon de faire les deux en même temps est d'avoir un type spécifique.


32
2018-04-30 14:50



Un point que je pense devrait être ajouté à ce que autre  réponses mention est que tout en

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

c'est vrai aussi

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

Le fonctionnement de l'intuition de l'OP - qui est tout à fait valide bien sûr - 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 types: Supposons que notre langage permette d'ajouter un chat à notre liste de chiens. Qu'est-ce que cela 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 quadrupèdes.

Pour le dire autrement: A 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 de type, ou plutôt, le (s) type (s) d'un objet est une fonction (dynamique) de sa valeur.


26
2018-03-30 07:14



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

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

Les tableaux sont réifiable et covariant.
Reifiable signifie que leurs informations de type sont entièrement disponibles au moment de l'exécution.
Par conséquent, les tableaux fournissent la sécurité du type d'exécution mais pas la sécurité du type au moment de 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é et invariant.
Les génériques ne peuvent donc pas fournir de sécurité de type runtime, mais ils fournissent une sécurité de type compilation.
Dans le code ci-dessous si les génériques étaient covariants, il sera possible de faire pollution de tas à la ligne 3.

    List<Dog> dogs = new ArrayList<>();
    List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
    animals.add(new Cat());

5
2017-09-29 19:55



Les réponses données ici ne m'ont pas 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 que passer Consumerle sable Suppliers pour Animals. Si tu as un Mammal consommateur, mais Duck fournisseur, ils ne devraient pas convenir bien que les deux soient 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 les 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 en sorte que nous ne pouvons utiliser qu'un fournisseur qui nous fournit le bon type d'objet pour le consommateur.

OTOH, on pourrait 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 restreindre qu'il peut ê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 les relations intuitives Life -> Animal -> Mammal -> Dog, Cat etc., nous pourrions même mettre un Mammal dans une Life consommateur, mais pas un String dans une Life consommateur.


4
2018-02-14 21:26



La logique de base d'un tel comportement est que Generics suivre 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 un tel processus d'effacement. Donc, revenons à 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
}

Maintenant, si java permet à l'appelant d'ajouter la liste de type Animal à cette méthode, alors vous pourriez ajouter une mauvaise chose dans la collection et au moment de l'exécution aussi, il va s'exécuter en raison de l'effacement de type. Tandis que dans le cas de tableaux, vous obtiendrez une exception d'exécution pour de tels scénarios ...

Donc, en substance, ce comportement est mis en œuvre de sorte que l'on ne peut pas ajouter une mauvaise chose dans la collection. Maintenant, je crois que l'effacement de type existe de manière à donner la compatibilité avec l'héritage java sans génériques ....


4
2017-12-04 10:43



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
2017-07-12 04:14



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

C'est utile quand vous voulez passer la liste dans un constructeur ou la passer en revue


1
2018-01-28 21:11



le répondre  ainsi que d'autres réponses sont correctes. Je vais ajouter à ces réponses une solution qui, je pense, sera utile. Je pense que cela arrive 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écomposent. Même la suppression est OK.

Dans la plupart des cas, nous pouvons utiliser Collection<? extends T> plutôt que Collection<T> et cela devrait être le premier choix. Cependant, je trouve des cas où ce n'est pas facile de le faire. Il est temps de débattre pour savoir si c'est toujours la meilleure chose à faire. Je présente ici une classe DownCastCollection qui peut prendre convertir un Collection<? extends T> à un Collection<T> (Nous pouvons définir des classes similaires pour List, Set, NavigableSet, ..) à utiliser lorsque 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 le 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 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
2017-12-15 11:14