¿Es List<Dog> una subclase de List<Animal>? ¿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 la herencia/polimorfismo.
Suponga la siguiente jerarquía:
Animal (padre)
Perro - Gato (Niños)
Entonces supongamos que tengo un método doSomething(List<Animal> animals)
. Según todas las reglas de herencia y polimorfismo, asumiría que a List<Dog>
es a List<Animal>
y a List<Cat>
es a List<Animal>
, por lo que 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 subclase 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?
No, a noList<Dog>
es a . Considere lo que puede hacer con un ... puede agregarle cualquier animal... incluido un gato. Ahora bien, ¿se puede lógicamente añadir un gato a una camada de cachorros? Absolutamente no.List<Animal>
List<Animal>
// 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 gato muy confundido.
Ahora, no puedes agregar a Cat
a List<? extends Animal>
porque no sabes que es un List<Cat>
. Puedes recuperar un valor y saber que será un Animal
, pero no puedes agregar animales arbitrarios. Lo contrario es cierto List<? super Animal>
: en ese caso, puede agregarle un Animal
archivo de forma segura, pero no sabe nada sobre lo que podría recuperarse de él, porque podría ser un archivo List<Object>
.
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 que List<Animal>
se podrían reemplazar con List<Dog>
). El problema es que la covarianza no es segura para las listas mutables en general. Supongamos que tiene un List<Dog>
, y se está utilizando como List<Animal>
. ¿Qué sucede cuando intentas agregar un gato a esto List<Animal>
que en realidad es 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 las ? extends Foo
declaraciones de métodos, pero eso agrega complejidad adicional.
La razón por la que a List<Dog>
no es a List<Animal>
es que, por ejemplo, puedes insertar a Cat
en a List<Animal>
, pero no en a List<Dog>
... puedes usar comodines para hacer que los genéricos sean más extensibles siempre que sea posible; por ejemplo, leer de a List<Dog>
es similar a leer de a List<Animal>
, pero no escribir.
Los Genéricos en el lenguaje Java y la Sección sobre Genéricos de los Tutoriales de Java tienen una explicación muy buena y detallada de por qué algunas cosas son o no polimórficas o están permitidas con los genéricos.
Un punto que creo que debería agregarse a lo que mencionan otras respuestas es que si bien
List<Dog>
no es unList<Animal>
en Java
también es cierto que
Una lista de perros es una lista de animales en inglés (bajo una interpretación razonable)
La intuición del OP, que por supuesto es completamente válida, es la última frase. Sin embargo, si aplicamos esta intuición obtenemos un lenguaje que no es similar a Java en su sistema de tipos: supongamos que nuestro lenguaje permite agregar un gato a nuestra lista de perros. ¿Qué significaría eso? Significaría que la lista dejaría de ser una lista de perros y seguiría siendo simplemente una lista de animales. Y una lista de mamíferos y una lista de cuadrápedos.
Para decirlo de otra manera: A List<Dog>
en Java no significa "una lista de perros" en inglés, significa "una lista de perros y nada más que perros".
De manera más general, la intuición de OP se presta hacia un lenguaje en el que las operaciones sobre objetos pueden cambiar su tipo , o más bien, el tipo(s) de un objeto es una función (dinámica) de su valor.
Yo 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 se compila bien, pero arroja un error de ejecución ( java.lang.ArrayStoreException: java.lang.Boolean
en la segunda línea). No es seguro para tipos. El objetivo de Generics es agregar seguridad de tipos en tiempo de compilación; de lo contrario, podría seguir con una clase simple sin genéricos.
Ahora hay momentos en los que necesitas ser más flexible y para eso están los ? super Class
y ? extends Class
. El primero es cuando necesita insertar en un tipo Collection
(por ejemplo), y el segundo es para cuando necesita leerlo, de manera segura. Pero la única manera de hacer ambas cosas al mismo tiempo es tener un tipo específico.