Tout moyen simple d'expliquer pourquoi Je ne peux pas faire List animals = new ArrayList ()? [dupliquer]

Cette question a déjà une réponse ici:

Je sais pourquoi on ne devrait pas faire ça. Mais est-il à expliquer à un profane pourquoi ce n'est pas possible. Vous pouvez expliquer cela à un profane facilement: Animal animal = new Dog();. Un chien est un animal, mais une liste de chiens n'est pas une liste d'animaux.

22
demandé sur Carl Manaster 2010-02-27 12:04:11

13 réponses

Imaginez que vous créez une liste de Chiens. Vous déclarez alors cela comme List et le remettez à un collègue. Il, pas déraisonnablement , croit qu'il peut y mettre unChat .

Ensuite, Il le donne à vous, et vous avez maintenant une liste de Chiens, avec Cat dans le milieu. Le Chaos s'ensuit.

Il est important de noter que cette restriction est là en raison de la mutabilité de la liste. Dans Scala (par exemple), vous pouvez déclarer qu'une liste de Chiens est une liste de Animaux. C'est parce que les listes Scala sont (par défaut) immuables, et donc ajouter un Cat à une liste de Chiens vous donnerait une nouvelle liste de animaux.

47
répondu Brian Agnew 2010-02-27 10:46:20

La réponse que vous cherchez concerne les concepts appelés covariance et contravariance. Certains langages les supportent (. net 4 ajoute le support, par exemple), mais certains des problèmes de base sont démontrés par le code comme ceci:

List<Animal> animals = new List<Dog>();

animals.Add(myDog); // works fine - this is a list of Dogs
animals.Add(myCat); // would compile fine if this were allowed, but would crash!

Parce Que Cat dériverait de animal, une vérification à la compilation suggérerait qu'il peut être ajouté à la liste. Mais, au moment de l'exécution, vous ne pouvez pas ajouter un Chat à une liste de Chiens!

Donc, bien que cela puisse sembler intuitivement simple, ces problèmes sont en fait très complexe en faire son affaire.

Il y a un aperçu MSDN de co/contravariance dans. NET 4 ici: http://msdn.microsoft.com/en-us/library/dd799517 (VS.100).aspx - tout est également applicable à java, même si Je ne sais pas à quoi ressemble le support de Java.

8
répondu Dan Puzey 2010-02-27 09:40:37

La meilleure réponse profane que je puisse donner est la suivante: parce que dans la conception de génériques, ils ne veulent pas répéter la même décision qui a été prise au système de type tableau de Java qui l'a rendu dangereux .

C'est possible avec les tableaux:

Object[] objArray = new String[] { "Hello!" };
objArray[0] = new Object();

Ce code compile très bien en raison de la façon dont le système de type array fonctionne en Java. Cela déclencherait un ArrayStoreException au moment de l'exécution.

La décision a été prise de ne pas autoriser un tel comportement dangereux pour les génériques.

Voir aussi ailleurs: Les tableaux Java cassent la sécurité de Type , que beaucoup considèrent comme l'un des défauts de conception Java .

8
répondu polygenelubricants 2010-02-27 15:57:10

Ce que vous essayez de faire est le suivant:

List<? extends Animal> animals = new ArrayList<Dog>()

Ça devrait marcher.

5
répondu Reverend Gonzo 2010-02-27 09:44:31

Un List est un objet où vous pouvez insérer n'importe quel animal, par exemple un chat ou une pieuvre. Un ArrayList ne l'est pas.

4
répondu Thomas Padron-McCarthy 2010-02-27 09:16:16

Supposons que vous pourriez faire ceci. Une des choses que quelqu'un a remis un List<Animal> s'attendrait raisonnablement à pouvoir faire est d'y ajouter un Giraffe. Que devrait-il se passer lorsque quelqu'un essaie d'ajouter un Giraffe à animals? Une erreur d'exécution? Cela semblerait aller à l'encontre du but de la saisie à la compilation.

3
répondu AakashM 2010-02-27 09:18:17

Je dirais que la réponse la plus simple est d'ignorer les chats et les chiens, ils ne sont pas pertinents. Ce qui est important, c'est la liste elle-même.

List<Dog> 

Et

List<Animal> 

Sont différents types, que le chien dérive de L'Animal n'a aucune incidence sur cela.

Cette déclaration n'est pas valide

List<Animal> dogs = new List<Dog>();

Pour la même raison que celui-ci est

AnimalList dogs = new DogList();

Alors que Dog peut hériter de Animal, la classe de liste générée par

List<Animal> 

N'hérite pas de la classe list générée par

List<Dog>

C'est une erreur de supposer que parce que deux classes sont liées, leur utilisation comme paramètres génériques rendra ces classes génériques également liées. Alors que vous pourriez certainement ajouter un chien à un

List<Animal>

Cela n'implique pas que

List<Dog> 

Est une sous-classe de

List<Animal>
3
répondu Ramon Leon 2010-03-03 03:29:37

Notez que si vous avez

List<Dog> dogs = new ArrayList<Dog>()

Alors, si vous pouviez faire

List<Animal> animals = dogs;

Ce n' pas activer dogs en List<Animal>. La structure de données sous-jacente aux animaux est toujours un ArrayList<Dog>, donc si vous essayez d'insérer un Elephant dans animals, Vous l'insérez réellement dans un ArrayList<Dog> qui ne fonctionnera pas (l'éléphant est évidemment trop gros ; -).

2
répondu Rune 2010-02-27 09:30:44

Tout d'Abord, définissons notre règne animal:

interface Animal {
}

class Dog implements Animal{
    Integer dogTag() {
        return 0;
    }
}

class Doberman extends Dog {        
}

Considérons deux interfaces paramétrées:

interface Container<T> {
    T get();
}

interface Comparator<T> {
    int compare(T a, T b);
}

Et les implémentations de ceux-ci où T est Dog.

class DogContainer implements Container<Dog> {
    private Dog dog;

    public Dog get() {
        dog = new Dog();
        return dog;
    }
}

class DogComparator implements Comparator<Dog> {
    public int compare(Dog a, Dog b) {
        return a.dogTag().compareTo(b.dogTag());
    }
}

Ce que vous demandez est tout à fait raisonnable dans le contexte de cette interface Container:

Container<Dog> kennel = new DogContainer();

// Invalid Java because of invariance.
// Container<Animal> zoo = new DogContainer();

// But we can annotate the type argument in the type of zoo to make
// to make it co-variant.
Container<? extends Animal> zoo = new DogContainer();

Alors pourquoi Java ne le fait-il pas automatiquement? Considérez ce que cela signifierait pour Comparator.

Comparator<Dog> dogComp = new DogComparator();

// Invalid Java, and nonsensical -- we couldn't use our DogComparator to compare cats!
// Comparator<Animal> animalComp = new DogComparator();

// Invalid Java, because Comparator is invariant in T
// Comparator<Doberman> dobermanComp = new DogComparator();

// So we introduce a contra-variance annotation on the type of dobermanComp.
Comparator<? super Doberman> dobermanComp = new DogComparator();

Si Java autorisait automatiquement Container<Dog> à être affecté à Container<Animal>, on s'attendrait également à ce qu'un Comparator<Dog> pourrait être affecté à un Comparator<Animal>, ce qui n'a aucun sens - comment un Comparator<Dog> pourrait-il comparer deux chats?

Quelle est Donc la différence entre Container et Comparator? Conteneur produit les valeurs de type T, alors que Comparator consomme eux. Celles-ci correspondent aux covariantes et contre-variantes usages du paramètre type.

Parfois, le paramètre type est utilisé dans les deux positions, ce qui rend l'interface invariant.

interface Adder<T> {
   T plus(T a, T b);
}

Adder<Integer> addInt = new Adder<Integer>() {
   public Integer plus(Integer a, Integer b) {
        return a + b;
   }
};
Adder<? extends Object> aObj = addInt;
// Obscure compile error, because it there Adder is not usable
// unless T is invariant.
//aObj.plus(new Object(), new Object());

Pour des raisons de rétrocompatibilité, Java est par défaut invariance . Vous devez explicitement choisir la variance appropriée avec ? extends X ou ? super X sur les types des variables, des champs, des paramètres ou des retours de méthode.

C'est un vrai problème-chaque fois que quelqu'un utilise le type générique, il doit prendre cette décision! Certes, les auteurs de Container et Comparator devraient pouvoir le déclarer une fois pour toutes.

Ceci est appelé "Declaration site Variance", et est disponible dans Scala.

trait Container[+T] { ... }
trait Comparator[-T] { ... }
2
répondu retronym 2010-02-27 12:56:23

Si vous ne pouviez pas muter la liste, votre raisonnement serait parfaitement sain. Malheureusement, un List<> est manipulé impérativement. Ce qui signifie que vous pouvez changer un List<Animal> en ajoutant un nouveau Animal. Si vous avez été autorisé à utiliser un List<Dog> comme List<Animal> vous pourriez vous retrouver avec une liste qui contient également une Cat.

Si List<> était incapable de mutation (comme à la Scala), alors vous pourriez traiter Un List<Dog> comme List<Animal>. Par exemple, C# rend ce comportement possible avec covariant et contravariant arguments de type génériques.

Ceci est une instance du principal de substitution Liskov plus général .

Le fait que la mutation vous cause un problème ici se produit ailleurs. Considérez les types Square et Rectangle.

Est un Square un Rectangle? Certainement -- d'un point de vue mathématique.

Vous pouvez définir une classe Rectangle qui offre des propriétés getWidth et getHeight lisibles.

Vous pouvez même ajouter des méthodes qui calculent son area ou perimeter, en fonction sur ces propriétés.

Vous pouvez alors définir une classe Square qui sous-classe Rectangle et fait en sorte que getWidth et getHeight renvoient la même valeur.

Mais que se passe-t-il lorsque vous commencez à autoriser la mutation via setWidth ou setHeight?

Maintenant, Square n'est plus une sous-classe raisonnable de Rectangle. La mutation d'une de ces propriétés devrait changer silencieusement l'autre afin de maintenir l'invariant, et le principal de substitution de Liskov serait violé. Modification de la largeur d'un Square aurait un effet secondaire inattendu. Pour rester un carré, vous devez également changer la hauteur, mais vous avez seulement demandé de changer la largeur!

Vous ne pouvez pas utiliser votre Square chaque fois que vous pourriez avoir utilisé un Rectangle. Donc, , en présence de la mutation un Square n'est pas Rectangle!

Vous pouvez créer une nouvelle méthode sur Rectangle qui sait comment cloner le rectangle avec une nouvelle largeur ou une nouvelle hauteur, puis votre Square pourrait transférer en toute sécurité à un Rectangle pendant le clonage processus, mais maintenant vous ne mutez plus la valeur d'origine.

De Même un List<Dog> ne peut pas être un List<Animal> lors de son interface vous permet d'ajouter de nouveaux éléments à la liste.

2
répondu Edward KMETT 2010-03-03 01:03:26

C'est parce que les types génériques sont invariant.

1
répondu helpermethod 2010-02-27 10:10:25

Réponse En Anglais:

Si ' List<Dog> est un List<Animal>', le premier doit supporter (hériter) toutes les opérations du second. L'ajout d'un chat peut être fait à ce dernier, mais pas à l'ancien. Donc, la relation "est une" échoue.

Réponse De Programmation:

Type De Sécurité

Un choix de conception par défaut de langage conservateur qui arrête cette corruption:

List<Dog> dogs = new List<>();
dogs.add(new Dog("mutley"));
List<Animal> animals = dogs;
animals.add(new Cat("felix"));  
// Yikes!! animals and dogs refer to same object.  dogs now contains a cat!!

Pour avoir une relation de sous-type, doit sastify 'castability' / 'substituability' critère.

  1. Substitution d'objet légal - toutes les opérations sur ancêtre prises en charge sur decendant:

    // Legal - one object, two references (cast to different type)
    Dog dog = new Dog();
    Animal animal = dog;  
    
  2. Substitution de collection LÉGALE - Toutes les opérations sur ancêtre prises en charge sur descendant:

    // Legal - one object, two references (cast to different type)
    List<Animal> list = new List<Animal>()
    Collection<Animal> coll = list;  
    
  3. Substitution Générique illégale (cast de paramètre de type) - ops non pris en charge dans decendant:

    // Illegal - one object, two references (cast to different type), but not typesafe
    List<Dog> dogs = new List<Dog>()
    List<Animal> animals = list;  // would-be ancestor has broader ops than decendant
    

Cependant

Selon la conception de la classe générique, les paramètres de type peuvent être utilisés dans des "positions sûres", ce qui signifie que la coulée/substitution peut parfois réussir sans corrompre la sécurité de type. Covariance signifie instatition Générique {[8] } peut remplacer G<T> Si U est un même type ou un sous-type de T. Contravariance signifie instanciation Générique G<U> peut remplacer G<T> Si U est un même type ou un supertype de T. Ce sont les positions sûres pour les 2 cas:

  • Positions covariantes:

    • méthode retour type (sortie de type générique) - les sous-types doivent être également / plus restrictifs,de sorte que leurs types de retour sont conformes à ancestor
    • Type de champs immuables (défini par la classe propriétaire, puis 'sortie interne uniquement') - les sous-types doivent être plus restrictifs, donc lorsqu'ils définissent des champs immuables, ils sont conformes à l'ancêtre

    Dans ces cas, il est sûr d'autoriser la substituabilité d'un paramètre de type avec un décendant comme ceci:

    SomeCovariantType<Dog> decendant = new SomeCovariantType<>;
    SomeCovariantType<? extends Animal> ancestor = decendant;
    

    Le caractère générique plus 'extends' donne utilisation-covariance spécifiée par le site.

  • Positions Contrvariantes:

    • Type de paramètre de méthode (entrée au type générique) - les sous-types doivent être également / plus accommodants afin qu'ils ne se cassent pas lorsque les paramètres passés de l'ancêtre
    • limites de paramètre de type supérieur (instanciation de type interne) - les sous-types doivent être également/plus accommodants, de sorte qu'ils ne se cassent pas lorsque les ancêtres définissent des valeurs variables

    Dans ces cas, c'est sûr pour permettre la substituabilité d'un paramètre de type avec un ancêtre comme ceci:

    SomeContravariantType<Animal> decendant = new SomeContravariantType<>;
    SomeContravariantType<? super Dog> ancestor = decendant;
    

    Le caractère générique plus 'super' donne une contravariance spécifiée par le site d'utilisation.

L'utilisation de ces 2 idiomes nécessite un effort et un soin supplémentaires de la part du développeur pour obtenir un "pouvoir de substituabilité". Java nécessite un effort manuel du développeur pour s'assurer que les paramètres de type sont vraiment utilisés dans les positions covariantes/contravariantes, respectivement (d'où type-safe). Je ne sais pas pourquoi-par exemple, le compilateur scala vérifie cela :-/. Vous dites essentiellement au compilateur 'croyez-moi, je sais ce que je fais, c'est sûr de type'.

  • Positions invariantes

    • Type de champ mutable (entrée et sortie internes ) - peut être lu et écrit par toutes les classes d'ancêtre et de sous-Type-La Lecture est covariante, l'écriture est contravariante; le résultat est invariant
    • (également si le paramètre type est utilisé à la fois dans les positions covariantes et contravariantes, cela se traduit par invariance)
0
répondu Glen Best 2013-07-24 06:55:33

En héritant, vous créez réellement un type commun pour plusieurs classes . Ici, vous avez un type d'animal commun . vous l'utilisez en créant un tableau dans le type D'Animal et en conservant des valeurs de types similaires (types hérités Chien, Chat, etc..).

Par exemple:

 dim animalobj as new List(Animal)
  animalobj(0)=new dog()
   animalobj(1)=new Cat()

.......

Compris?

-1
répondu Vibin Jith 2010-02-27 09:39:45