Estoy un poco confundido acerca de cómo los genéricos de Java manejan la herencia / polimorfismo.
Asuma la siguiente jerarquía:
Animal (padre)
Perro - Gato (Niños)
Supongamos que tengo un método doSomething(List<Animal> animals)
. Según todas las reglas de herencia y polimorfismo, supondría que a List<Dog>
es a List<Animal>
y a List<Cat>
es a List<Animal>
, por lo que cualquiera de ellos 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 es generalmente implícito, pero cuando se trata de genéricos debe especificarse?
fuente
Respuestas:
No, a no
List<Dog>
es a . Considere lo que puede hacer con un : puede agregarle cualquier animal ... incluido un gato. Ahora, ¿puedes agregar lógicamente un gato a una camada de cachorros? Absolutamente no.List<Animal>
List<Animal>
De repente tienes un muy gato confundido.
Ahora, no puede agregar a
Cat
aList<? extends Animal>
porque no sabe que es aList<Cat>
. Puede recuperar un valor y saber que será unAnimal
, pero no puede agregar animales arbitrarios. Lo contrario es cierto paraList<? super Animal>
: en ese caso, puede agregarle un mensajeAnimal
de forma segura, pero no sabe nada sobre lo que podría recuperarse, ya que podría ser unList<Object>
.fuente
Lo que está buscando 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 conDog
), lo mismo se aplica a las expresiones que usan esos objetos (por lo queList<Animal>
podría reemplazarse porList<Dog>
). El problema es que la covarianza no es segura para listas mutables en general. Supongamos que tiene unList<Dog>
, y se está utilizando como aList<Animal>
. ¿Qué sucede cuando intentas agregar un gato a esto,List<Animal>
que es realmente unList<Dog>
? Permitir automáticamente que los parámetros de tipo sean covariantes rompe el sistema de tipos.Sería útil agregar una sintaxis para permitir que los parámetros de tipo se especifiquen como covariantes, lo que evita las
? extends Foo
declaraciones en el método, pero eso agrega complejidad adicional.fuente
La razón por la cual a
List<Dog>
no es aList<Animal>
, es que, por ejemplo, puede insertar unCat
en unList<Animal>
, pero no en unList<Dog>
... puede usar comodines para hacer que los genéricos sean más extensibles cuando sea posible; por ejemplo, leer de aList<Dog>
es similar a leer de aList<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 profunda de por qué algunas cosas son o no polimórficas o están permitidas con genéricos.
fuente
Un punto que creo que debería agregarse a lo que otras respuestas mencionan es que
también es cierto que
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 Java-esque 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 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 cuadrúpedos.
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".En términos más generales, la intuición de OP se presta hacia un lenguaje en el que las operaciones en los objetos pueden cambiar su tipo , o más bien, el tipo (s) de un objeto es una función (dinámica) de su valor.
fuente
Yo diría que el punto de Generics es que no permite eso. Considere la situación con las matrices, que permiten ese tipo de covarianza:
Ese código se compila bien, pero arroja un error de tiempo de ejecución (
java.lang.ArrayStoreException: java.lang.Boolean
en la segunda línea). No es de tipo seguro. El objetivo de Generics es agregar la seguridad del tipo de tiempo de compilación, de lo contrario, podría seguir con una clase simple sin genéricos.Ahora bien, hay momentos en los que necesita para ser más flexible y que es lo que el
? super Class
y? extends Class
son para. El primero es cuando necesita insertar en un tipoCollection
(por ejemplo), y el segundo es para cuando necesita leer de él, de manera segura. Pero la única forma de hacer ambas cosas al mismo tiempo es tener un tipo específico.fuente
Para comprender el problema, es útil hacer una comparación con las matrices.
List<Dog>
es no subclase deList<Animal>
.Pero
Dog[]
es una subclase deAnimal[]
.Las matrices son reificables y covariantes .
Reificable significa que su información de tipo está totalmente 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.
Es al revés para los genéricos: los
genéricos son borrados e invariables .
Por lo tanto, los genéricos no pueden proporcionar seguridad de 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 por pilas en la línea 3.
fuente
Las respuestas dadas aquí no me convencieron por completo. Entonces, en cambio, hago otro ejemplo.
suena bien, ¿no? Pero solo puedes pasar
Consumer
s ySupplier
s porAnimal
s. Si tiene unMammal
consumidor, pero unDuck
proveedor, no deben encajar, aunque ambos son animales. Para no permitir esto, se han agregado restricciones adicionales.En lugar de lo anterior, tenemos que definir relaciones entre los tipos que usamos.
P.ej.,
se asegura de que solo podamos usar un proveedor que nos proporcione el tipo de objeto adecuado para el consumidor.
OTOH, también podríamos hacer
a dónde vamos en la otra dirección: definimos el tipo de
Supplier
y restringimos que se pueda poner en elConsumer
.Incluso podemos hacer
donde, teniendo las relaciones intuitivas
Life
->Animal
->Mammal
->Dog
,Cat
etc., incluso podríamos ponerMammal
a unLife
consumidor, pero noString
a unLife
consumidor.fuente
(Consumer<Runnable>, Supplier<Dog>)
mientrasDog
es subtipo deAnimal & Runnable
La lógica básica para tal comportamiento es que
Generics
sigue un mecanismo de borrado de tipo. Por lo tanto, en el tiempo de ejecución no tiene forma si identifica el tipo decollection
diferencia dearrays
donde no existe dicho proceso de borrado. Volviendo a tu pregunta ...Supongamos que hay un método como se indica a continuación:
Ahora, si Java permite a la persona que llama agregar Lista de tipo Animal a este método, entonces puede agregar algo incorrecto a la colección y en el tiempo de ejecución también se ejecutará debido a la eliminación de tipo. Mientras que en el caso de las matrices, obtendrá una excepción de tiempo de ejecución para tales escenarios ...
Así, en esencia, este comportamiento se implementa para que no se pueda agregar algo incorrecto a la colección. Ahora creo que existe la eliminación de tipos para dar compatibilidad con Java heredado sin genéricos ...
fuente
El subtipo es invariante para los tipos parametrizados. Incluso si la clase
Dog
es un subtipo deAnimal
, el tipo parametrizadoList<Dog>
no es un subtipo deList<Animal>
. Por el contrario, las matrices utilizan el subtipo covariante , por lo que el tipo de matrizDog[]
es un subtipo deAnimal[]
.El subtipo invariable garantiza que no se infrinjan las restricciones de tipo impuestas por Java. Considere el siguiente código dado por @Jon Skeet:
Según lo indicado por @ Jon Skeet, este código es ilegal, porque de lo contrario violaría las restricciones de tipo al devolver un gato cuando lo esperaba un perro.
Es instructivo comparar lo anterior con un código análogo para matrices.
El código es legal. Sin embargo, arroja una excepción de tienda de matriz . Una matriz lleva su tipo en tiempo de ejecución de esta manera JVM puede hacer cumplir la seguridad de tipo de subtipo covariante.
Para entender esto aún más, veamos el bytecode generado por
javap
la clase a continuación:Usando el comando
javap -c Demonstration
, esto muestra el siguiente código de bytes de Java:Observe que el código traducido de los cuerpos de los métodos es idéntico. El compilador reemplazó cada tipo parametrizado por su borrado . Esta propiedad es crucial, lo que significa que no rompió la compatibilidad con versiones anteriores.
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.
fuente
En realidad, puede usar una interfaz para lograr lo que desea.
}
entonces puedes usar las colecciones usando
fuente
Si está seguro de que los elementos de la lista son subclases de ese supertipo dado, puede emitir la lista con este enfoque:
Esto es útil cuando desea pasar la lista en un constructor o iterar sobre ella
fuente
La respuesta , así como otras respuestas, son correctas. Voy a agregar a esas respuestas una solución que creo que será útil. Creo que esto aparece a menudo en la programación. Una cosa a tener en cuenta es que para las Colecciones (Listas, Conjuntos, etc.) el problema principal es agregar a la Colección. Ahí es donde las cosas se rompen. Incluso eliminar está bien.
En la mayoría de los casos, podemos usar en
Collection<? extends T>
lugar de esoCollection<T>
y esa debería ser la primera opción. Sin embargo, estoy encontrando casos en los que no es fácil hacer eso. Está en discusión si eso es siempre lo mejor que se puede hacer. Estoy presentando aquí una clase DownCastCollection que puede convertir aCollection<? extends T>
a aCollection<T>
(podemos definir clases similares para List, Set, NavigableSet, ...) para usar cuando usar el enfoque estándar es muy inconveniente. A continuación se muestra un ejemplo de cómo usarlo (también podríamos usarloCollection<? extends Object>
en este caso, pero lo mantengo simple para ilustrar usando DownCastCollection.Ahora la clase:
}
fuente
Collections.unmodifiableCollection
Collection<? extends E>
Sin embargo, ya maneja ese comportamiento correctamente, a menos que lo use de una manera que no sea de tipo seguro (por ejemplo, convertirlo en otra cosa). La única ventaja que veo es que, cuando llamas a laadd
operación, arroja una excepción incluso si la lanzaste.Tomemos el ejemplo del tutorial de JavaSE
Entonces, ¿por qué una lista de perros (círculos) no debe considerarse implícitamente? Una lista de animales (formas) se debe a esta situación:
Así que los "arquitectos" de Java tenían 2 opciones que abordan este problema:
no considere que un subtipo es implícitamente su supertipo, y dé un error de compilación, como sucede ahora
considere que el subtipo es su supertipo y restrinja al compilar el método "agregar" (por lo tanto, 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 un error de compilación para hacerlo ese).
Por razones obvias, eso eligió la primera forma.
fuente
También debemos tener en cuenta cómo el compilador amenaza las clases genéricas: en "instancia" un tipo diferente cada vez que rellenamos los argumentos genéricos.
Así tenemos
ListOfAnimal
,ListOfDog
,ListOfCat
, etc, que son clases distintas que terminan siendo "creada" por el compilador cuando especificamos los argumentos genéricos. Y esta es una jerarquía plana (en realidad respecto aList
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.
List
instancias. EspecializarseList
en rellenando el argumento genérico no extiende la clase, solo hace que funcione para ese argumento genérico en particular.fuente
El problema ha sido bien identificado. Pero hay una solución; hacer doSomething genérica:
ahora puede llamar a doSomething con List <Dog> o List <Cat> o List <Animal>.
fuente
otra solución es construir una nueva lista
fuente
Además de la respuesta de Jon Skeet, que usa este código de ejemplo:
En el nivel más profundo, el problema aquí es eso
dogs
yanimals
compartir una referencia. Eso significa que una forma de hacer que esto funcione sería copiar la lista completa, lo que rompería la igualdad de referencia:Después de llamar
List<Animal> animals = new ArrayList<>(dogs);
, no se puede asignar posteriormente directamenteanimals
a cualquieradogs
ocats
:por lo tanto, no puede colocar el subtipo incorrecto
Animal
en la lista, porque no hay un subtipo incorrecto;? extends Animal
se puede agregar cualquier objeto de subtipoanimals
.Obviamente, esto cambia la semántica, ya que las listas
animals
yadogs
no se comparten, por lo que agregar a una lista no agrega a la otra (que es exactamente lo que desea, para evitar el problema de que seCat
pueda agregar a una lista que solo es se supone que contieneDog
objetos). Además, copiar toda la lista puede ser ineficiente. Sin embargo, esto resuelve el problema de equivalencia de tipo, al romper la igualdad de referencia.fuente