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




8 Answers

Lo que buscas se llama parámetros de tipo covariante . Esto significa que si un tipo de objeto se puede sustituir por otro en un método (por ejemplo, Animal se puede reemplazar con Dog ), lo mismo se aplica a las expresiones que usan esos objetos (por lo tanto, List<Animal> podría reemplazarse con List<Dog> ). El problema es que la covarianza no es segura para las listas mutables en general. Supongamos que tiene una List<Dog> , y se está utilizando como una List<Animal> . ¿Qué sucede cuando intenta agregar un Gato a esta List<Animal> que es realmente una List<Dog> ? Automáticamente permitiendo que los parámetros de tipo sean covariantes rompe el sistema de tipo.

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

java generics inheritance polymorphism

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

Supongamos la siguiente jerarquía:

Animal (padre)

Perro - Gato (Niños)

Supongamos que tengo un método doSomething(List<Animal> animals) . Por todas las reglas de herencia y polimorfismo, supongo que un List<Dog> List<Animal> es un List<Animal> y un List<Cat> List<Animal> es un List<Animal> , por lo que se puede pasar cualquiera de los dos a este método. No tan. Si quiero lograr este comportamiento, debo 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 es generalmente implícito, pero cuando se trata de genéricos, debe especificarse?




Yo diría que el punto central de los genéricos es que no permite eso. Considere la situación con matrices, que permiten ese tipo de covarianza:

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

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

Ahora hay momentos en los que necesitas 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 insertarse en una Collection tipos (por ejemplo), y la última es para cuando necesita leer de ella, de una manera segura. Pero la única manera de hacer ambas cosas al mismo tiempo es tener un tipo específico.




Para entender el problema es útil hacer comparaciones con arreglos.

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

Los arreglos 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 son erased e invariantes .
Por lo tanto, los genéricos no pueden proporcionar seguridad de tipo de tiempo de ejecución, pero sí proporcionan seguridad de tipo de tiempo de compilación.
En el código a continuación, si los genéricos son covariantes, será posible hacer que la 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());



La lógica básica para tal comportamiento es que los Generics siguen un mecanismo de borrado de tipo. Por lo tanto, en el tiempo de ejecución no tiene forma de identificar el tipo de collection diferencia de las arrays en las que no existe tal proceso de borrado. Así que volviendo a tu pregunta ...

Así que supongamos que hay un método como se indica a continuación:

add(List<Animal>){
    //You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}

Ahora, si java permite a la persona que llama agregar la Lista de tipo Animal a este método, entonces puede agregar algo incorrecto en la colección y en el tiempo de ejecución también se ejecutará debido al borrado de tipo. Mientras que en el caso de matrices, obtendrá una excepción de tiempo de ejecución para tales escenarios ...

Por lo tanto, en esencia, este comportamiento se implementa para que uno no pueda agregar algo incorrecto a la colección. Ahora creo que el borrado de tipos existe para dar compatibilidad con Java heredado sin genéricos ...




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

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

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




Subtipo es invariant para tipos parametrizados. Incluso si la clase Dog es un subtipo de Animal , el tipo parametrizado List<Dog> no es un subtipo de List<Animal> . En contraste, el subtipo invariant es utilizado por los arreglos, por lo que el tipo de arreglo Dog[] es un subtipo de Animal[] .

Los subtipos invariantes aseguran que las restricciones de tipo impuestas por Java no sean violadas. Considere el siguiente código dado por @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);

Como lo indica @Jon Skeet, este código es ilegal, porque de lo contrario violaría las restricciones de tipo al devolver un gato cuando un perro esperaba.

Es instructivo comparar lo anterior con un código análogo para matrices.

Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];

El código es legal. Sin embargo, lanza una excepción de tienda de matriz . Una matriz transporta su tipo en tiempo de ejecución de esta manera JVM puede imponer la seguridad de tipos de subtipos covariantes.

Para entender esto, veamos el javap de javap generado por javap de la clase a continuación:

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");
    }
}

Usando el comando javap -c Demonstration , esto muestra el siguiente bytecode de Java:

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
}

Observe que el código traducido de los cuerpos del método es idéntico. El compilador reemplazó cada tipo parametrizado por su erasure . Esta propiedad es crucial, lo que significa que no rompió la compatibilidad hacia atrás.

En conclusión, la seguridad en tiempo de ejecución no es posible para los tipos parametrizados, ya que el compilador reemplaza cada tipo parametrizado por su borrado. Esto hace que los tipos parametrizados no sean más que azúcar sintáctica.




También debemos tener en cuenta cómo el compilador amenaza a las clases genéricas: en "crea instancias" de un tipo diferente cada vez que llenamos los argumentos genéricos.

Por lo tanto, tenemos ListOfAnimal , ListOfDog , ListOfCat , etc., que son clases distintas que terminan siendo "creadas" por el compilador cuando especificamos los argumentos genéricos. Y esta es una jerarquía plana (en realidad con respecto a la List no es una jerarquía en absoluto).

Otro argumento por el que la covarianza no tiene sentido en el caso de las clases genéricas es el hecho de que en la base todas las clases son iguales, son instancias de List . La especialización de una List completando el argumento genérico no extiende la clase, solo hace que funcione para ese argumento genérico en particular.




Otra solución es construir una nueva lista.

List<Dog> dogs = new ArrayList<Dog>(); 
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());



Related