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

770

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). D'après toutes les règles d'héritage et de polymorphisme, je suppose que a List<Dog> est a List<Animal>et a List<Cat> est a List<Animal>- et que l'une ou l'autre pourrait être transmise à cette méthode. Mais non. Si je veux obtenir ce comportement, je dois explicitement dire à la méthode d'accepter une liste de n'importe quelle sous-classe 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 lorsqu'il s'agit de génériques, il doit être spécifié?

froadie
la source
17
Et une question de grammaire totalement indépendante qui me dérange maintenant - mon titre devrait-il être "pourquoi les génériques de Java ne sont-ils pas " ou "pourquoi ne sont-ils pas les génériques de Java" ?? Les «génériques» sont-ils pluriels à cause du s ou singuliers parce que c'est une entité?
froadie
25
les génériques comme ceux de Java sont une forme très médiocre de polymorphisme paramétrique. Ne leur faites pas trop confiance (comme je le faisais auparavant), car un jour vous frapperez fort leurs limites pathétiques: le chirurgien étend Handable <Scalpel>, Handable <Sponge> KABOOM! Ne calcule pas [TM]. Il y a votre limitation des génériques Java. Tout OOA / OOD peut être traduit correctement en Java (et MI peut être fait très bien en utilisant des interfaces Java) mais les génériques ne le coupent pas. Ils sont parfaits pour les "collections" et la programmation procédurale qui l'ont dit (ce que font de toute façon la plupart des programmeurs Java ...).
SyntaxT3rr0r
8
La super classe de List <Dog> n'est pas List <Animal> mais List <?> (C'est-à-dire liste de type inconnu). Les génériques effacent les informations de type dans le code compilé. Ceci est fait pour que le code qui utilise des génériques (java 5 et supérieur) soit compatible avec les versions antérieures de java sans génériques.
rai.skumar
9
@froadie puisque personne ne semblait répondre ... ce devrait certainement être "pourquoi les génériques de Java ne sont-ils pas ...". L'autre problème est que "générique" est en fait un adjectif, et donc "générique" fait référence à un nom pluriel abandonné modifié par "générique". Vous pourriez dire «cette fonction est générique», mais ce serait plus lourd que de dire «cette fonction est générique». Cependant, c'est un peu lourd de dire "Java a des fonctions et des classes génériques", au lieu de "Java a des génériques". En tant que personne qui a écrit sa thèse de maîtrise sur les adjectifs, je pense que vous êtes tombé sur une question très intéressante!
dantiston

Réponses:

916

Non, un List<Dog>n'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 chat très confus.

Maintenant, vous ne pouvez pas ajouter un 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 y ajouter un Animalen toute sécurité, mais vous ne savez rien de ce qui pourrait être récupéré, car il pourrait s'agir d'un List<Object>.

Jon Skeet
la source
50
Fait intéressant, chaque liste de chiens est en effet une liste d'animaux, tout comme l'intuition nous le dit. Le fait est que toutes les listes d'animaux ne sont pas une liste de chiens, donc la mutattion de la liste en ajoutant un chat est le problème.
Ingo
66
@Ingo: Non, pas vraiment: vous pouvez ajouter un chat à une liste d'animaux, mais vous ne pouvez pas ajouter un chat à une liste de chiens. Une liste de chiens n'est qu'une liste d'animaux si vous la considérez en lecture seule.
Jon Skeet
12
@ JonSkeet - Bien sûr, mais qui exige que la création d'une nouvelle liste à partir d'un chat et d'une liste de chiens modifie réellement la liste des chiens? Il s'agit d'une décision d'implémentation arbitraire en Java. Celui qui va à l'encontre de la logique et de l'intuition.
Ingo
7
@Ingo: Je n'aurais pas utilisé cela "certainement" pour commencer. Si vous avez une liste qui dit en haut "Hôtels où nous pourrions vouloir aller" et que quelqu'un y a ajouté une piscine, pensez-vous que cela est valable? Non, c'est une liste d'hôtels, ce n'est pas une liste de bâtiments. Et ce n'est pas comme si j'avais même dit "Une liste de chiens n'est pas une liste d'animaux" - je la mets en termes de code , dans une police de code. Je ne pense vraiment pas qu'il y ait une ambiguïté ici. L'utilisation d'une sous-classe serait de toute façon incorrecte - il s'agit de la compatibilité des affectations, pas de la sous-classe.
Jon Skeet
14
@ruakh: Le problème est que vous passez alors au moment de l'exécution quelque chose qui peut être bloqué au moment de la compilation. Et je dirais que la covariance de tableau était une erreur de conception pour commencer.
Jon Skeet
84

Ce que vous recherchez s'appelle des paramètres de type covariant . Cela signifie que si un type d'objet peut être remplacé par un autre dans une méthode (par exemple, il Animalpeut être remplacé par Dog), il en va de même pour les expressions utilisant ces objets (il List<Animal>pourrait donc être remplacé par List<Dog>). Le problème est que la covariance n'est pas sûre pour les listes mutables en général. Supposons que vous en ayez un List<Dog>et qu'il soit utilisé 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 la covariance des paramètres de type rompt le système de type.

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

Michael Ekstrand
la source
44

La raison pour laquelle un List<Dog>n'est pas un List<Animal>est que, par exemple, vous pouvez insérer un Catdans 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 lorsque cela est possible; par exemple, la lecture d'un a List<Dog>est similaire à la lecture d'un List<Animal>- mais pas l'écriture.

Les génériques dans le langage Java et la section sur les génériques des didacticiels Java ont une très bonne explication approfondie pour expliquer pourquoi certaines choses sont ou ne sont pas polymorphes ou autorisées avec les génériques.

Michael Aaron Safyan
la source
36

Je pense qu'il convient d'ajouter à ce que mentionnent les autres réponses,

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 (enfin, sous une interprétation raisonnable)

La façon dont fonctionne l'intuition du PO - qui est tout à fait valable 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 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 quadrupèdes.

En d'autres termes: 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.

einpoklum
la source
Oui, le langage humain est plus flou. Mais encore, une fois que vous ajoutez un animal différent à la liste des chiens, c'est toujours une liste d'animaux, mais plus une liste de chiens. La différence étant, un humain, avec la logique floue, n'a généralement aucun problème à s'en rendre compte.
Vlasec
En tant que personne qui trouve les comparaisons constantes avec les tableaux encore plus déroutantes, cette réponse m'a cloué. Mon problème était l'intuition du langage.
FLonLon
32

Je dirais que l'intérêt de Generics est qu'il ne permet pas cela. Considérez la situation avec les 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.Booleansur 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 vous en tenir à une classe ordinaire sans génériques.

Il y a maintenant des moments où vous devez être plus flexible et c'est à cela que servent les ? super Classet ? extends Class. Le premier est lorsque vous devez insérer dans un type Collection(par exemple), et le second est lorsque vous devez lire à partir de celui-ci, d'une manière sûre. Mais la seule façon de faire les deux en même temps est d'avoir un type spécifique.

Yishai
la source
13
On peut soutenir que la covariance des tableaux est un bogue de conception de langage. Notez qu'en raison de l'effacement de type, le même comportement est techniquement impossible pour la collecte générique.
Michael Borgwardt
" Je dirais que l'intérêt de Generics est qu'il ne permet pas cela. ". Vous ne pouvez jamais être sûr: Java et les systèmes de types de Scala ne sont pas solides: la crise existentielle des pointeurs nuls (présenté à OOPSLA 2016) (depuis corrigé, il semble)
David Tonhofer
15

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

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

Les tableaux sont réifiables et covariants .
Reifiable signifie que leurs informations de type sont entièrement disponibles au moment de l'exécution.
Par conséquent, les tableaux offrent une sécurité de type à l'exécution, mais pas une sécurité de type à 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 pas assurer la 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 faire de la 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());
Outdev
la source
2
On pourrait faire valoir que, précisément à cause de cela, les tableaux en Java sont cassés ,
leonbloy
Les tableaux étant covariants est une «fonctionnalité» du compilateur.
Cristik
6

Les réponses données ici ne m'ont pas complètement convaincu. Au lieu de cela, je fais un autre exemple.

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

sonne bien, non? Mais vous ne pouvez passer que Consumers et Suppliers pour Animals. Si vous avez un Mammalconsommateur, mais un Duckfournisseur, ils ne devraient pas convenir bien que les deux soient des animaux. Afin de refuser 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, 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 du Supplieret limitons qu'il peut être mis dans le Consumer.

On peut 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, Catetc., nous pourrions même mettre un Mammaldans un Lifeconsommateur, mais pas un Stringdans un Lifeconsommateur.

glglgl
la source
1
Parmi les 4 versions, # 2 est probablement incorrecte. par exemple, nous ne pouvons pas l'appeler avec (Consumer<Runnable>, Supplier<Dog>)while Dogest un sous-type deAnimal & Runnable
ZhongYu
5

La logique de base d'un tel comportement est de Genericssuivre un mécanisme d'effacement de type. Donc, au moment de l'exécution, vous n'avez aucun moyen d'identifier le type de collectioncontrairement à arraysoù il n'y a pas un tel processus d'effacement. 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
}

Maintenant, si java permet à l'appelant d'ajouter la liste de type Animal à cette méthode, vous pouvez ajouter une erreur dans la collection et au moment de l'exécution, elle s'exécutera également 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, en substance, ce comportement est implémenté de sorte que l'on ne puisse pas ajouter de mauvaises choses à la collection. Maintenant, je crois que l'effacement de type existe pour donner une compatibilité avec java hérité sans génériques ....

Hitesh
la source
4

Le sous-typage est invariant pour les types paramétrés. Même difficile, la classe Dogest un sous-type de Animal, le type paramétré List<Dog>n'est pas un sous-type de List<Animal>. En revanche, le sous-typage covariant est utilisé par les tableaux, donc le type de tableau Dog[]est un sous-type deAnimal[] .

Le sous-typage invariant garantit que les contraintes de type imposé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 il 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 de magasin de tableaux . Un tableau transporte son type au moment de l'exécution de cette manière, la JVM peut appliquer la sécurité de type du sous-typage covariant.

Pour mieux comprendre cela, regardons le bytecode généré par javapla 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 effacement . Cette propriété est cruciale, ce qui signifie qu'elle n'a pas rompu la compatibilité descendante.

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 fait que les types paramétrés ne sont rien d'autre que du sucre syntaxique.

Racine G
la source
3

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());
Angel Koh
la source
1

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

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

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

sagits
la source
2
Cela créera plus de problèmes qu'il n'en résout réellement
Ferrybig
Si vous essayez d'ajouter un chat à la liste, cela créera certainement des problèmes, mais à des fins de bouclage, je pense que c'est la seule réponse non verbeuse.
sagits
1

La réponse 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 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 s'effondrent. Même la suppression est OK.

Dans la plupart des cas, nous pouvons utiliser Collection<? extends T>plutôt alors Collection<T>et cela devrait être le premier choix. Cependant, je trouve des cas où ce n'est pas facile à faire. Il appartient au débat de savoir si c'est toujours la meilleure chose à faire. Je présente ici une classe DownCastCollection qui peut prendre convertir un Collection<? extends T>en 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ênante. Ci-dessous est un exemple de la façon de l'utiliser (nous pourrions également utiliser Collection<? extends Object>dans ce cas, mais je reste simple 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();

}

}

dan b
la source
C'est une bonne idée, à tel point qu'elle existe déjà dans Java SE. ; )Collections.unmodifiableCollection
Radiodef
1
C'est vrai, mais la collection que je définis peut être modifiée.
dan b
Oui, il peut être modifié. Collection<? extends E>gère déjà ce comportement correctement, à moins que vous ne l'utilisiez d'une manière qui ne soit pas sûre pour le type (par exemple en le convertissant en autre chose). Le seul avantage que je vois est que, lorsque vous appelez l' addopération, elle lève une exception même si vous l'avez lancée.
Vlasec
0

Prenons l'exemple du tutoriel JavaSE

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 doit pas être considérée implicitement comme une liste d'animaux (formes), c'est à cause de cette situation:

// drawAll method call
drawAll(circleList);


public void drawAll(List<Shape> shapes) {
   shapes.add(new Rectangle());    
}

Les "architectes" Java disposaient donc de 2 options pour résoudre ce problème:

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

  2. considérez le sous-type comme étant son supertype et limitez lors de la compilation la méthode "add" (donc dans la méthode drawAll, si une liste de cercles, sous-type de forme, est transmise, le compilateur devrait le détecter et vous limiter avec une erreur de compilation à faire cette).

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

aurelius
la source
0

Nous devons également prendre en compte 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 des classes distinctes qui finissent par être « créé » par le compilateur lorsque nous précisons les arguments génériques. Et c'est une hiérarchie plate (en fait, ce Listn'est pas du tout une hiérarchie).

Un autre argument pour lequel la covariance n'a pas de sens dans le cas des classes génériques est le fait qu'à la base toutes les classes sont les mêmes - sont des Listinstances. La spécialisation d'un Listen remplissant l'argument générique ne prolonge pas la classe, elle le fait simplement fonctionner pour cet argument générique particulier.

Cristik
la source
0

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

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

vous pouvez maintenant appeler doSomething avec List <Dog> ou List <Cat> ou List <Animal>.

gerardw
la source
0

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

Suite à la réponse de Jon Skeet, qui utilise cet exemple de code:

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

Au niveau le plus profond, le problème ici est cela dogset animalspartage une référence. Cela signifie qu'une façon de faire ce travail serait de copier la liste entière, ce qui romprait l'égalité de référence:

// This code is fine
List<Dog> dogs = new ArrayList<Dog>();
dogs.add(new Dog());
List<Animal> animals = new ArrayList<>(dogs); // Copy list
animals.add(new Cat());
Dog dog = dogs.get(0);   // This is fine now, because it does not return the Cat

Après avoir appelé List<Animal> animals = new ArrayList<>(dogs);, vous ne pouvez pas ensuite attribuer directement animalsà une ou l' autre dogsou cats:

// These are both illegal
dogs = animals;
cats = animals;

par conséquent, vous ne pouvez pas mettre le mauvais sous-type de Animaldans la liste, car il n'y a pas de sous-type incorrect - tout objet de sous-type ? extends Animalpeut être ajouté animals.

Évidemment, cela change la sémantique, car les listes animalset dogsne sont plus partagées, donc l'ajout à une liste ne s'ajoute pas à l'autre (ce qui est exactement ce que vous voulez, pour éviter le problème qu'un Catpourrait être ajouté à une liste qui n'est que censé contenir des Dogobjets). De plus, la copie de la liste entière peut être inefficace. Cependant, cela résout le problème d'équivalence de type, en brisant l'égalité de référence.

Luke Hutchison
la source