Pregunta ¿Es List una subclase de List ? ¿Por qué los genéricos de Java no son implícitamente polimórficos?


Estoy un poco confundido acerca de cómo los genéricos de Java manejan herencia / polimorfismo.

Supongamos la siguiente jerarquía:

Animal (Padre)

Perro - Gato (Niños)

Entonces, supongamos que tengo un método doSomething(List<Animal> animals). Por todas las reglas de herencia y polimorfismo, supongo que List<Dog>  es un List<Animal> y un List<Cat>  es un List<Animal> - y cualquiera de los dos podría pasarse a este método. No tan. Si quiero lograr este comportamiento, tengo que decirle explícitamente al método que acepte una lista de cualquier subconjunto de Animal diciendo doSomething(List<? extends Animal> animals).

Entiendo que este es el comportamiento de Java. Mi pregunta es por qué? ¿Por qué el polimorfismo generalmente está implícito, pero cuando se trata de genéricos, debe especificarse?


616
2018-04-30 14:39


origen


Respuestas:


No, a List<Dog> es no un List<Animal>. Considera lo que puedes hacer con un List<Animal> - puedes añadir alguna animal a eso ... incluso un gato. Ahora, ¿puedes lógicamente agregar un gato a una camada de cachorros? Absolutamente no.

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

De repente tienes un muy gato confundido

Ahora tu hipocresía Agrega un Cat a un List<? extends Animal> porque no sabes que es un List<Cat>. Puede recuperar un valor y saber que será un Animal, pero no puedes agregar animales arbitrarios. Lo contrario es cierto para List<? super Animal> - en ese caso, puede agregar un Animal de forma segura, pero no sabe nada de lo que se puede recuperar de él, porque podría ser un List<Object>.


756
2018-04-30 14:44



Lo que estás buscando se llama tipo covariante parámetros. El problema es que no son seguros en cuanto a tipos en el caso general, específicamente para listas mutables. Supongamos que tiene un List<Dog>, y está permitido funcionar como List<Animal>. ¿Qué sucede cuando intentas agregar un gato a esto? List<Animal> que es realmente un List<Dog>? Permitir automáticamente que los parámetros de tipo sean covariantes rompe el sistema de tipos.

Sería útil agregar sintaxis para permitir que los parámetros de tipo se especifiquen como covariantes, lo que evita el ? extends Foo en declaraciones de métodos, pero eso agrega complejidad adicional.


67
2018-04-30 14:44



La razón por la cual List<Dog> no es un List<Animal>, es que, por ejemplo, puedes insertar un Cat en un List<Animal>, pero no en un List<Dog>... puedes usar comodines para hacer que los genéricos sean más extensibles cuando sea posible; por ejemplo, leyendo de un List<Dog> es similar a la lectura de un List<Animal> - pero no escribiendo

los Genéricos en el lenguaje Java y el Sección sobre genéricos de los tutoriales de Java tener una explicación muy buena y detallada de por qué algunas cosas son o no son polimórficas o están permitidas con genéricos.


42
2018-04-30 14:46



Diría que el objetivo de Generics es que no permite eso. Considere la situación con matrices, que sí permiten ese tipo de covarianza:

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

Ese código compila bien, pero arroja un error de tiempo de ejecución (java.lang.ArrayStoreException: java.lang.Boolean en la segunda línea). No es seguro. El objetivo de Generics es agregar el tipo de seguridad de tiempo de compilación, de lo contrario, podría simplemente quedarse con una clase simple sin genéricos.

Ahora hay momentos en los que necesita ser más flexible y eso es lo que ? super Class y ? extends Class son para. El primero es cuando necesita insertar en un tipo Collection (por ejemplo), y el último es para cuando necesita leer de él, de una manera segura. Pero la única forma de hacer ambas cosas al mismo tiempo es tener un tipo específico.


32
2018-04-30 14:50



Un punto que creo debería agregarse a otro  respuestas mencionar es que mientras

List<Dog> no es-a List<Animal>  en Java

también es cierto que

Una lista de perros es una lista de animales en inglés (bueno, bajo una interpretación razonable)

La forma en que funciona la intuición del OP, que es completamente válida, por supuesto, es la última oración. Sin embargo, si aplicamos esta intuición obtenemos un lenguaje que no es de tipo Java en su sistema de tipo: supongamos que nuestro lenguaje permite agregar un gato a nuestra lista de perros. ¿Qué significa eso? Significaría que la lista deja de ser una lista de perros y sigue siendo simplemente una lista de animales. Y una lista de mamíferos, y una lista de quadrapeds.

Para decirlo de otra manera: A List<Dog> en Java no significa "una lista de perros" en inglés, significa "una lista que puede tener perros, y nada más".

Más generalmente, La intuición de OP se presta a un lenguaje en el que las operaciones en objetos pueden cambiar su tipo, o más bien, el tipo (s) de un objeto es una función (dinámica) de su valor.


26
2018-03-30 07:14



Para comprender el problema, es útil hacer una comparación con las matrices.

List<Dog> es no subclase de List<Animal>.
Pero  Dog[]  es subclase de Animal[].

Las matrices son reificable y covariante.
Reificable significa que su tipo de información está completamente disponible en tiempo de ejecución.
Por lo tanto, las matrices proporcionan seguridad de tipo de tiempo de ejecución, pero no seguridad de tipo de tiempo de compilación.

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

Es al revés para los genéricos:
Los genéricos son borrado e invariante.
Por lo tanto, los genéricos no pueden proporcionar seguridad del tipo de tiempo de ejecución, pero proporcionan seguridad de tipo de tiempo de compilación.
En el siguiente código si los genéricos son covariantes, será posible hacer contaminación del montón en la línea 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



Las respuestas dadas aquí no me convencieron del todo. Entonces, en cambio, hago otro ejemplo.

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

suena bien, ¿no? Pero solo puedes pasar Consumers y Suppliers para Animals. Si tienes un Mammal consumidor, pero una Duck proveedor, no deberían caber aunque ambos son animales. Para no permitir esto, se han agregado restricciones adicionales.

En lugar de lo anterior, tenemos que definir las relaciones entre los tipos que usamos.

P.ej.,

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

nos aseguramos de que solo podemos utilizar un proveedor que nos proporciona el tipo correcto de objeto para el consumidor.

OTOH, podríamos hacer también

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

donde vamos de otra manera: definimos el tipo de Supplier y restrinja que se puede poner en el Consumer.

Incluso podemos hacer

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

donde, teniendo las relaciones intuitivas Life -> Animal -> Mammal -> Dog, Cat etc., incluso podríamos poner un Mammal en un Life consumidor, pero no un String en un Life consumidor.


4
2018-02-14 21:26