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


Answers

Lo que está buscando se llama parámetros de tipo covariante . El problema es que no son seguros en cuanto a tipos en el caso general, específicamente para listas mutables. Supongamos que tiene una List<Dog> , y está permitido que funcione como una List<Animal> . ¿Qué sucede cuando intentas agregar un Gato a esta List<Animal> que en realidad es una 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 ? extends Foo en declaraciones de métodos, pero eso agrega complejidad adicional.

Question

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, supondría que una List<Dog> es una List<Animal> y una List<Cat> es una List<Animal> , por lo que cualquiera podría pasar a este método. No tan. Si deseo 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?




Si está seguro de que los elementos de la lista son subclases de ese súper tipo dado, puede lanzar la lista con este enfoque:

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

Esto es útil cuando quiere pasar la lista en un constructor o iterar sobre ella




Tomemos el ejemplo del tutorial 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) {
        ...
    }
}

Entonces, ¿por qué una lista de perros (círculos) no debe considerarse implícitamente como una lista de animales (formas) debido a esta situación?

// drawAll method call
drawAll(circleList);


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

Así que los "arquitectos" de Java tenían 2 opciones que solucionan este problema:

  1. no considere que un subtipo sea implícitamente supertipo, y proporcione un error de compilación, como sucede ahora

  2. considere el subtipo como su supertipo y restrinja al compilar el método "agregar" (así que en el método drawAll, si se pasara una lista de círculos, subtipo de forma, el compilador debería detectar eso y restringirlo con error de compilación para hacer ese).

Por razones obvias, que eligió la primera manera.




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 ? super Class y ? extends Class ? extends Class son para. La primera es cuando necesita insertar en una Collection tipo (por ejemplo), y la última es para cuando necesita leer de ella, de una manera segura. Pero la única forma de hacer ambas cosas al mismo tiempo es tener un tipo específico.




El problema ha sido bien identificado. Pero hay una solución; hacer algo ALGO genérico

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

ahora puede llamar a DoSomething con List <Dog> o List <Cat> o List <Animal>.




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 puede aprobar los Supplier y Supplier de Animal s. Si tiene un consumidor de Mammal , pero un proveedor de Duck , no deberían caber aunque ambos sean 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());
}

hacia donde vamos en la otra dirección: definimos el tipo de Supplier y restringimos que se pueda 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., podríamos incluso poner un Mammal en un consumidor de Life , pero no un consumidor de String en una Life .




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

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

Las matrices son reifiable y covariantes .
reifiable significa que su información de tipo 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 se erased e invariantes .
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 generar contaminació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());



Links