java - ¿Por qué las matrices son covariantes pero los genéricos son invariables?
arrays generics (8)
A través de wikipedia :
Las primeras versiones de Java y C # no incluían los genéricos (también conocido como polimorfismo paramétrico).
En tal contexto, hacer matrices invariables descarta programas polimórficos útiles. Por ejemplo, considere escribir una función para mezclar una matriz, o una función que pruebe dos matrices para la igualdad utilizando el método
Object.equals
en los elementos. La implementación no depende del tipo exacto de elemento almacenado en la matriz, por lo que debería ser posible escribir una única función que funcione en todos los tipos de matrices. Es fácil implementar funciones de tipo
boolean equalArrays (Object[] a1, Object[] a2); void shuffleArray(Object[] a);
Sin embargo, si los tipos de matriz se trataran como invariantes, solo sería posible llamar a estas funciones en una matriz exactamente del tipo
Object[]
. Uno no podría, por ejemplo, mezclar una serie de cadenas.Por lo tanto, tanto Java como C # tratan los tipos de matriz de forma coherente. Por ejemplo, en C #
string[]
es un subtipo deobject[]
, y en JavaString[]
es un subtipo deObject[]
.
Esto responde a la pregunta "¿Por qué las matrices son covariantes?", O más exactamente, "¿Por qué las matrices se hicieron covariantes en ese momento ?"
Cuando se introdujeron los genéricos, a propósito no se hicieron covariantes por las razones señaladas en esta respuesta por Jon Skeet :
No, una
List<Dog>
no es unaList<Animal>
. Considere lo que puede hacer con unaList<Animal>
: puede agregarle cualquier animal ... incluido 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 List<Dog>(); 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.
La motivación original para hacer matrices covariantes descritas en el artículo de wikipedia no se aplicaba a los genéricos porque los wildcards hacían posible la expresión de covarianza (y contravarianza), por ejemplo:
boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);
De Effective Java por Joshua Bloch,
- Las matrices difieren del tipo genérico en dos formas importantes. Las primeras matrices son covariantes. Los genéricos son invariables.
Covariante simplemente significa que si X es un subtipo de Y, entonces X [] también será un subtipo de Y []. Las matrices son covariantes. Como una cadena es un subtipo de Object So
String[] is subtype of Object[]
Invariante simplemente significa independientemente de que X sea subtipo de Y o no,
List<X> will not be subType of List<Y>.
Mi pregunta es por qué la decisión de hacer matrices covariantes en Java? Hay otras publicaciones SO tales como ¿Por qué las matrices son invariables, pero las listas son covariantes? , pero parecen estar enfocados en Scala y no puedo seguirlos.
Creo que tomaron una decisión equivocada en el primer lugar que hizo una matriz covariante. Se rompe el tipo de seguridad como se describe here y se quedaron atrapados con eso debido a la compatibilidad con versiones anteriores y después de eso intentaron no cometer el mismo error genérico. Y esa es una de las razones por las cuales Joshua Bloch prefiere las listas a los arrays en el Ítem 25 del libro "Effective Java (second edition)"
La razón es que cada matriz sabe su tipo de elemento durante el tiempo de ejecución, mientras que la colección genérica no lo hace debido a la eliminación del tipo. Por ejemplo:
String[] strings = new String[2];
Object[] objects = strings; // valid, String[] is Object[]
objects[0] = 12; // error, would cause java.lang.ArrayStoreException: java.lang.Integer during runtime
Si esto fue permitido con colecciones genéricas:
List<String> strings = new ArrayList<String>();
List<Object> objects = strings; // let''s say it is valid
objects.add(12); // invalid, Integer should not be put into List<String> but there is no information during runtime to catch this
Pero esto causaría problemas más tarde cuando alguien intente acceder a la lista:
String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String
Las matrices son covariantes por al menos dos razones:
Es útil para colecciones que contienen información que nunca cambiará para ser covariante. Para que una colección de T sea covariante, su almacén de respaldo también debe ser covariante. Si bien se podría diseñar una colección
T
inmutable que no usara unT[]
como almacén de respaldo (por ejemplo, utilizando un árbol o una lista vinculada), tal colección no sería tan efectiva como una respaldada por una matriz. Se podría argumentar que una mejor forma de proporcionar covariables colecciones inmutables hubiera sido definir un tipo de "matriz inmutable covariante" que podrían usar una tienda de respaldo, pero simplemente permitir la covarianza de matriz probablemente sea más fácil.Las matrices con frecuencia serán mutadas por código que no sabe qué tipo de cosas van a contener, pero no colocarán en la matriz nada que no haya sido leído en la misma matriz. Un buen ejemplo de esto es el código de clasificación. Conceptualmente, podría haber sido posible que los tipos de matriz incluyeran métodos para intercambiar o permutar elementos (tales métodos podrían ser igualmente aplicables a cualquier tipo de matriz), o definir un objeto "manipulador de matriz" que mantenga una referencia a una matriz y una o más cosas que se leyó, y podría incluir métodos para almacenar elementos previamente leídos en la matriz de la que provenían. Si las matrices no fueran covariantes, el código de usuario no podría definir dicho tipo, pero el tiempo de ejecución podría haber incluido algunos métodos especializados.
El hecho de que las matrices sean covariantes puede verse como un hack feo, pero en la mayoría de los casos facilita la creación de código de trabajo.
Mi opinión: cuando el código está esperando una matriz A [] y le da B [] donde B es una subclase de A, solo hay dos cosas de qué preocuparse: qué sucede cuando lee un elemento de matriz y qué sucede si escribe eso. Por lo tanto, no es difícil escribir reglas de lenguaje para garantizar que se preserve la seguridad de tipo en todos los casos (la regla principal es que se puede lanzar una ArrayStoreException
si intenta ArrayStoreException
una A en B []). Sin embargo, para un genérico, cuando declaras una clase SomeClass<T>
, puede haber varias formas en T
se usa T
en el cuerpo de la clase, y supongo que es demasiado complicado para calcular todas las combinaciones posibles. para escribir reglas sobre cuándo se permiten cosas y cuándo no.
Puede ser this ayuda: -
Los genéricos no son covariantes
Las matrices en el lenguaje Java son covariantes, lo que significa que si el Número entero se extiende (lo que hace), no solo es un Número entero, sino que un Número entero [] también es un Number[]
, y usted puede pasar libremente o asigne un Integer[]
donde se Number[]
un Number[]
. (Más formalmente, si Number es un supertipo de Integer, entonces Number[]
es un supertipo de Integer[]
). Puede pensar que lo mismo es cierto para los tipos genéricos también, que List<Number>
es un supertipo de List<Integer>
, y que puede pasar una List<Integer>
donde se espera una List<Number>
. Desafortunadamente, no funciona de esa manera.
Resulta que hay una buena razón por la cual no funciona de esa manera: rompería el tipo de seguridad que se supone que proporcionan los genéricos. Imagine que puede asignar una List<Integer>
a una List<Number>
. Entonces, el siguiente código le permitiría poner algo que no era un Entero en una List<Integer>
:
List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));
Como es un List<Number>
, agregarle un Float parece perfectamente legal. Pero si tuviera un alias con li
, rompería la promesa de seguridad de tipo implícita en la definición de li, que es una lista de enteros, por lo que los tipos genéricos no pueden ser covariantes.
Una característica importante de los tipos paramétricos es la capacidad de escribir algoritmos polimórficos, es decir, algoritmos que operan en una estructura de datos independientemente de su valor de parámetro, como Arrays.sort()
.
Con los genéricos, eso se hace con tipos de comodines:
<E extends Comparable<E>> void sort(E[]);
Para ser realmente útil, los tipos comodín requieren la captura de comodines, y eso requiere la noción de un parámetro de tipo. Nada de eso estaba disponible en el momento en que las matrices se agregaron a Java, y las matrices de tipo de referencia covariantes permitieron una forma mucho más sencilla de permitir algoritmos polimórficos:
void sort(Comparable[]);
Sin embargo, esa simplicidad abrió una laguna en el sistema de tipo estático:
String[] strings = {"hello"};
Object[] objects = strings;
objects[0] = 1; // throws ArrayStoreException
requiriendo un control en tiempo de ejecución de cada acceso de escritura a una matriz de tipo de referencia.
En pocas palabras, el enfoque más nuevo incorporado por los genéricos hace que el sistema de tipo sea más complejo, pero también más seguro de tipo estático, mientras que el enfoque anterior era más simple y menos seguro de forma estática. Los diseñadores del lenguaje optaron por un enfoque más simple, teniendo cosas más importantes que hacer que cerrar un pequeño vacío en el sistema de tipos que rara vez causa problemas. Más tarde, cuando se estableció Java y se atendieron las necesidades urgentes, tenían los recursos para hacerlo bien para los genéricos (pero cambiarlo para las matrices habría roto los programas Java existentes).
Los genéricos son invariables : de JSL 4.10 :
... Subtipo no se extiende a través de tipos genéricos: T <: U no implica que
C<T>
<:C<U>
...
y unas pocas líneas más, JLS también explica que
Las matrices son covariantes (primera viñeta):
4.10.3 Subtipo entre tipos de matriz