¿Alguna forma sencilla de explicar por qué no puedo hacer una lista de animales = nueva lista de matrices ()?

Sé por qué uno no debería hacer eso. Pero ¿hay alguna manera de explicarle a un profano por qué esto no es posible? Puede explicar esto fácilmente a un profano: Animal animal = new Dog(); . Un perro es un tipo de animal, pero una lista de perros no es una lista de animales.

Imagina que creas una lista de perros . A continuación, declara esto como Lista y se lo pasa a un colega. Él, no sin razón , cree que puede poner un gato en él.

Luego te lo devuelve, y ahora tienes una lista de Perros , con un Gato en el medio. Caos sobreviene.

Es importante tener en cuenta que esta restricción existe debido a la mutabilidad de la lista. En Scala (por ejemplo), puede declarar que una lista de perros es una lista de animales . Esto se debe a que las listas de Scala son (de forma predeterminada) inmutables, por lo que agregar un Gato a una lista de Perros le daría una nueva lista de Animales .

La respuesta que está buscando tiene que ver con conceptos llamados covarianza y contravarianza. Algunos lenguajes son compatibles (.NET 4 agrega soporte, por ejemplo), pero algunos de los problemas básicos se demuestran con un código como este:

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

Como Cat derivaría de un animal, una verificación en tiempo de comstackción sugeriría que se puede agregar a List. Pero, en tiempo de ejecución, ¡no puedes agregar un gato a una lista de perros!

Entonces, aunque pueda parecer intuitivamente simple, estos problemas son en realidad muy complejos.

Hay una descripción general de MSDN de la co / contravariancia en .NET 4 aquí: http://msdn.microsoft.com/en-us/library/dd799517(VS.100).aspx ; también es aplicable a Java, aunque no lo hago. t saber cómo es el soporte de Java.

La mejor respuesta que puedo dar es esta: porque al diseñar generics no quieren repetir la misma decisión que se hizo con el sistema de tipo de matriz de Java que lo hizo inseguro .

Esto es posible con matrices:

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

Este código se comstack muy bien debido a la forma en que el sistema de tipo de matriz funciona en Java. Levantaría una ArrayStoreException en tiempo de ejecución.

La decisión fue tomada para no permitir tal comportamiento inseguro para los generics.

Ver también en otro lugar: Java Arrays Break Type Safety , que muchos consideran uno de los defectos de diseño de Java .

Lo que estás tratando de hacer es lo siguiente:

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

Eso debería funcionar.

Una Lista es un objeto donde puede insertar cualquier animal, por ejemplo, un gato o un pulpo. Un ArrayList no lo es.

Supongamos que pudieras hacer esto. Una de las cosas que alguien entregó una List razonablemente podría esperar que sea capaz de agregarle una Giraffe . ¿Qué debería pasar cuando alguien intenta agregar una Giraffe a los animals ? ¿Un error de tiempo de ejecución? Eso parecería frustrar el propósito del tipeo en tiempo de comstackción.

Yo diría que la respuesta más simple es ignorar a los gatos y perros, no son relevantes. Lo importante es la lista en sí misma.

 List 

y

 List 

son diferentes tipos, que Dog deriva de Animal no tiene nada que ver con esto.

Esta statement no es válida

 List dogs = new List(); 

por la misma razón que esta es

 AnimalList dogs = new DogList(); 

Mientras Dog puede heredar de Animal, la clase de lista generada por

 List 

no hereda de la clase de lista generada por

 List 

Es un error suponer que debido a que dos clases están relacionadas, el usarlas como parámetros generics hará que esas clases genéricas también se relacionen. Si bien podría agregar un perro a un

 List 

eso no implica que

 List 

es una subclase de

 List 

Tenga en cuenta que si tiene

 List dogs = new ArrayList() 

entonces, si pudieras hacer

 List animals = dogs; 

esto no convierte a los dogs en una List . La estructura de datos subyacente a los animales sigue siendo un ArrayList , así que si tratas de insertar un Elephant en animals , en realidad lo estás insertando en un ArrayList que no va a funcionar (el Elephant es obviamente demasiado grande; -).

Primero, definamos nuestro reino animal:

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

Considere dos interfaces parametrizadas:

 interface Container { T get(); } interface Comparator { int compare(T a, T b); } 

Y las implementaciones de estos donde T es Dog .

 class DogContainer implements Container { private Dog dog; public Dog get() { dog = new Dog(); return dog; } } class DogComparator implements Comparator { public int compare(Dog a, Dog b) { return a.dogTag().compareTo(b.dogTag()); } } 

Lo que está preguntando es bastante razonable en el contexto de esta interfaz de Container :

 Container kennel = new DogContainer(); // Invalid Java because of invariance. // Container 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(); 

Entonces, ¿por qué Java no hace esto automáticamente? Considere lo que esto significaría para Comparator .

 Comparator dogComp = new DogComparator(); // Invalid Java, and nonsensical -- we couldn't use our DogComparator to compare cats! // Comparator animalComp = new DogComparator(); // Invalid Java, because Comparator is invariant in T // Comparator dobermanComp = new DogComparator(); // So we introduce a contra-variance annotation on the type of dobermanComp. Comparator< ? super Doberman> dobermanComp = new DogComparator(); 

Si Java permitiera automáticamente que Container se asignara a Container , también se esperaría que un Comparator pudiera asignar a un Comparator , lo que no tiene sentido, ¿cómo podría un Comparator comparar dos gatos?

Entonces, ¿cuál es la diferencia entre Container y Comparator ? El contenedor produce valores del tipo T , mientras que el Comparator consume . Estos corresponden a usos covariantes y contravariantes del parámetro tipo.

En ocasiones, el parámetro tipo se usa en ambas posiciones, lo que hace que la interfaz sea invariable .

 interface Adder { T plus(T a, T b); } Adder addInt = new Adder() { 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()); 

Por razones de compatibilidad con versiones anteriores, Java se predetermina a la invarianza . Debe elegir explícitamente la varianza adecuada con ? extends X ? extends X o ? super X ? super X en los tipos de variables, campos, parámetros o resultados del método.

Esto es una verdadera molestia: cada vez que alguien usa un tipo genérico, ¡deben tomar esta decisión! Seguramente los autores de Container and Comparator deberían ser capaces de declarar esto de una vez por todas.

Esto se llama ‘Declaración de varianza del sitio’ y está disponible en Scala.

 trait Container[+T] { ... } trait Comparator[-T] { ... } 

Si no pudieras mutar la lista, tu razonamiento sería perfectamente correcto. Desafortunadamente una List<> es manipulada imperativamente. Lo que significa que puede cambiar una List añadiéndole un Animal nuevo. Si se le permitiera usar una List como una List podría terminar con una lista que también contiene un Cat .

Si List<> fuera incapaz de mutar (como en Scala), entonces podría tratar A List como List . Por ejemplo, C # hace que este comportamiento sea posible con argumentos de tipo generics covariantes y contravariantes.

Esta es una instancia del principio de sustitución de Liskov más general.

El hecho de que la mutación te cause un problema aquí sucede en otros lugares. Considere los tipos Square y Rectangle .

¿Es un Square un Rectangle ? Ciertamente, desde una perspectiva matemática.

Podría definir una clase Rectangle que ofrezca getWidth legibles getWidth y getHeight .

Incluso podría agregar métodos que calculen su area o perimeter , según esas propiedades.

Luego puede definir una clase Square que subclasifique Rectangle y haga que tanto getWidth como getHeight devuelvan el mismo valor.

Pero, ¿qué sucede cuando comienzas a permitir la mutación a través de setWidth o setHeight ?

Ahora, Square ya no es una subclase razonable de Rectangle . La mutación de una de esas propiedades tendría que cambiar silenciosamente la otra para mantener la invariante, y se violaría el principio de sustitución de Liskov. Cambiar el ancho de un Square tendría un efecto secundario inesperado. Para seguir siendo un cuadrado, deberías cambiar la altura también, ¡pero solo pediste cambiar el ancho!

No puedes usar tu Square siempre que puedas haber usado un Rectangle . Entonces, en presencia de una mutación, ¡ un Square no es un Rectangle !

Podría hacer un nuevo método en Rectangle que sepa cómo clonar el rectángulo con un nuevo ancho o una nueva altura, y luego su Square podría ceder de forma segura a un Rectangle durante el proceso de clonación, pero ahora ya no está mutando el valor original.

De manera similar, una List no puede ser una List cuando su interfaz le permite agregar nuevos elementos a la lista.

Esto se debe a que los tipos generics son invariables .

Respuesta en inglés:

Si ‘ List es una List ‘, el primero debe soportar (heredar) todas las operaciones de este último. Agregar un gato se puede hacer a este último, pero no antes. Entonces la relación ‘es una’ falla.

Respuesta de progtwigción:

Tipo de seguridad

Una opción de diseño predeterminado de lenguaje conservador que detiene esta corrupción:

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

Para tener una relación de subtipo, debe mejorar los criterios de ‘fundibilidad’ / ‘subsistencia’.

  1. Substición de objeto legal: todas las operaciones de antepasado compatibles con descendencia:

     // Legal - one object, two references (cast to different type) Dog dog = new Dog(); Animal animal = dog; 
  2. Sustitución de colección legal: todas las operaciones de antepasado compatibles con descendientes:

     // Legal - one object, two references (cast to different type) List list = new List() Collection coll = list; 
  3. Sustitución genérica ilegal (elenco del parámetro tipo) – operaciones no respaldadas en descendencia:

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

sin embargo

Dependiendo del diseño de la clase genérica, los parámetros de tipo pueden usarse en “posiciones seguras”, lo que significa que la conversión / sustitución a veces puede tener éxito sin corromper la seguridad del tipo. Covarianza significa que la instalación genérica G puede sustituir a G si U es un mismo tipo o subtipo de T. Contravariancia significa que la instancia genérica G puede G sustituir G si U es un mismo tipo o supertipo de T. Estas son las posiciones seguras para los 2 casos:

  • posiciones covariantes:

    • método de devolución tipo (salida de tipo genérico): los subtipos deben ser igualmente / más restrictivos, por lo que sus tipos de devolución cumplen con ancestro
    • tipo de campos inmutables (establecidos por clase de propietario, luego ‘internamente solo salida’): los subtipos deben ser más restrictivos, por lo que cuando establecen campos inmutables, cumplen con el antecesor

    En estos casos, es seguro permitir la sustituibilidad de un parámetro de tipo con una descendencia como esta:

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

    El comodín más ‘extensiones’ proporciona la covarianza especificada en el sitio de uso.

  • posiciones contributivas:

    • Método tipo de parámetro (entrada al tipo genérico): los subtipos deben ser igualmente / más acomodaticios para que no se rompan cuando pasan los parámetros del ancestro
    • límites de parámetros de tipo superior (instanciación de tipo interno): los subtipos deben ser igualmente / más adaptables, por lo que no se rompen cuando los antepasados ​​establecen valores de variables

    En estos casos, es seguro permitir la sustituibilidad de un parámetro de tipo con un antecesor como este:

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

    El comodín más ‘super’ da una contravariancia especificada en el sitio de uso.

El uso de estos 2 idiomas requiere un esfuerzo extra y el cuidado del desarrollador para obtener ‘poder de sustitución’. Java requiere un esfuerzo de desarrollador manual para garantizar que los parámetros de tipo se usen realmente en posiciones covariantes / contravariantes, respectivamente (de ahí que sean seguros para el tipo). No sé por qué, por ejemplo, el comstackdor scala comprueba esto: – /. Básicamente le estás diciendo al comstackdor “créeme, sé lo que estoy haciendo, esto es seguro”.

  • posiciones invariables

    • tipo de campo mutable (entrada y salida interna): puede leerse y escribirse con todas las clases de ancestros y subtipos; la lectura es covariante, la escritura es contravariante; el resultado es invariante
    • (también si el parámetro de tipo se usa tanto en posiciones covariantes como contravariantes, entonces esto da como resultado la invarianza)

Al heredar, en realidad estás creando tipos comunes para varias clases. Aquí tienes un tipo de animal común. lo estás usando creando una matriz en tipo de Animal y conservando valores de tipos similares (tipos heredados perro, gato, etc.).

P.ej:

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

…….

¿Lo tengo?