¿Por qué las matrices son covariantes pero los genéricos son invariables?

160

De Java efectivo por Joshua Bloch,

  1. Las matrices difieren del tipo genérico en dos formas importantes. Las primeras matrices son covariantes. Los genéricos son invariantes.
  2. 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 la cadena es un subtipo de Objeto Entonces

    String[] is subtype of Object[]

    Invariante simplemente significa que 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 invariantes, pero las listas son covariantes? , pero parecen centrarse en Scala y no puedo seguirlo.

Ansioso por aprender
fuente
1
¿No es esto porque los genéricos se agregaron más tarde?
Sotirios Delimanolis
1
¡Creo que comparar entre matrices y colecciones es injusto, las colecciones usan matrices en segundo plano!
Ahmed Adel Ismail
44
@ EL-conteDe-monteTereBentikh No todas las colecciones, por ejemplo LinkedList.
Paul Bellora
@PaulBellora Sé que los mapas son diferentes a los implementadores de la Colección, ¡pero leí en SCPJ6 que las Colecciones generalmente dependían de las matrices!
Ahmed Adel Ismail
Porque no hay ArrayStoreException; al insertar un elemento incorrecto en la Colección donde lo tiene una matriz. Así que la Colección puede encontrar esto solo en el momento de la recuperación y eso también debido al lanzamiento. Por lo tanto, los genéricos asegurarán la resolución de este problema.
Kanagavelu Sugumar

Respuestas:

150

Vía wikipedia :

Las primeras versiones de Java y C # no incluían genéricos (también conocido como polimorfismo paramétrico).

En tal entorno, hacer que las matrices sean invariables descarta programas polimórficos útiles. Por ejemplo, considere escribir una función para barajar una matriz, o una función que pruebe la igualdad de dos matrices utilizando el Object.equalsmétodo 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 sola 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 de exactamente el tipo Object[]. Uno no podría, por ejemplo, barajar una serie de cadenas.

Por lo tanto, Java y C # tratan los tipos de matriz de manera covariante. Por ejemplo, en C # string[]es un subtipo de object[], y en Java String[]es un subtipo de Object[].

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, a List<Dog>no es a List<Animal>. Considere lo que puede hacer con un List<Animal>: puede agregarle cualquier animal ... incluido un gato. Ahora, ¿puedes agregar lógicamente 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 las matrices covariantes descritas en el artículo de Wikipedia no se aplicaba a los genéricos porque los comodines hicieron posible la expresión de covarianza (y contravarianza), por ejemplo:

boolean equalLists(List<?> l1, List<?> l2);
void shuffleList(List<?> l);
Paul Bellora
fuente
3
Sí, las matrices permiten un comportamiento polimórfico, sin embargo, introduce excpeciones de tiempo de ejecución (a diferencia de las excepciones de tiempo de compilación con genéricos). por ejemplo:Object[] num = new Number[4]; num[1]= 5; num[2] = 5.0f; num[3]=43.4; System.out.println(Arrays.toString(num)); num[0]="hello";
eagertoLearn
21
Eso es correcto. Las matrices tienen tipos reificables y arrojan ArrayStoreExceptions según sea necesario. Claramente, esto se consideró un compromiso digno en ese momento. Compare eso con hoy: muchos consideran la covarianza de la matriz como un error, en retrospectiva.
Paul Bellora
1
¿Por qué "muchos" lo consideran un error? Es mucho más útil que no tener covarianza de matriz. ¿Con qué frecuencia has visto una ArrayStoreException; Son bastante raros. La ironía aquí es imperdonable: entre los peores errores jamás cometidos en Java está la variación del sitio de uso, también conocida como comodines.
Scott
3
@ScottMcKinney: "¿Por qué" muchos "lo consideran un error?" AIUI, esto se debe a que la covarianza de la matriz requiere pruebas de tipo dinámico en todas las operaciones de asignación de la matriz (aunque las optimizaciones del compilador quizás puedan ayudar), lo que puede causar una sobrecarga de tiempo de ejecución significativa.
Dominique Devriese
Gracias, Dominique, pero según mi observación, parece que la razón por la que "muchos" lo consideran un error es más similar a lo que algunos otros han dicho. Nuevamente, observando de nuevo la covarianza de la matriz, es mucho más útil que perjudicial. Nuevamente, el GRAN error real que cometió Java fue la variación genérica del sitio de uso mediante comodines. Eso ha causado más problemas de los que creo que "muchos" quieren admitir.
Scott
30

La razón es que cada matriz conoce su tipo de elemento durante el tiempo de ejecución, mientras que la colección genérica no lo sabe 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 se permitió 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 intentara acceder a la lista:

String first = strings.get(0); // would cause ClassCastException, trying to assign 12 to String
Katona
fuente
Creo que la respuesta de Paul Bellora es más apropiada cuando comenta sobre POR QUÉ Las matrices son covariantes. Si las matrices se hicieron invariantes, entonces está bien. tendrías que borrar el tipo con él. La razón principal de la propiedad Tipo de borrado es la compatibilidad con versiones anteriores, ¿correcto?
eagertoLearn
@ user2708477, sí, se introdujo el borrado de tipo debido a la compatibilidad con versiones anteriores. Y sí, mi respuesta intenta responder a la pregunta en el título, por qué los genéricos son invariables.
Katona
El hecho de que las matrices conozcan su tipo significa que, si bien la covarianza permite que el código solicite almacenar algo en una matriz donde no cabe, no significa que se permita que dicha tienda tenga lugar. En consecuencia, el nivel de peligro introducido al hacer que las matrices sean covariantes es mucho menor de lo que sería si no conocieran sus tipos.
supercat
@supercat, correcto, lo que quería señalar es que para los genéricos con borrado de tipo en su lugar, la covarianza no podría haberse implementado con la seguridad mínima de las comprobaciones de tiempo de ejecución
Katona
1
Personalmente, creo que esta respuesta proporciona la explicación correcta de por qué las matrices son covariantes cuando las colecciones no pueden serlo. ¡Gracias!
pregunta
22

Puede ser esta ayuda: -

Los genéricos no son covariantes

Las matrices en el lenguaje Java son covariantes, lo que significa que si Integer extiende un Número (lo que hace), entonces no solo un Entero también es un Número, sino que un Entero [] también es un Number[], y usted es libre de pasar o asignar un Integer[]donde Number[]se llama a. (Más formalmente, si Number es un supertipo de Integer, entonces Number[]es un supertype de Integer[]). También podría pensar que lo mismo es cierto para los tipos genéricos, que List<Number>es un supertype de List<Integer>, y que puede pasar un List<Integer>donde List<Number>se espera un a. Desafortunadamente, no funciona de esa manera.

Resulta que hay una buena razón por la que no funciona de esa manera: rompería el tipo que los genéricos de seguridad debían proporcionar. Imagina que puedes asignar un List<Integer>a a List<Number>. Entonces, el siguiente código le permitiría poner algo que no fuera un número entero en List<Integer>:

List<Integer> li = new ArrayList<Integer>();
List<Number> ln = li; // illegal
ln.add(new Float(3.1415));

Como ln es a List<Number>, agregar un Float parece perfectamente legal. Pero si se alias con li, entonces 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.

Rahul Tripathi
fuente
3
Para las matrices, obtienes un ArrayStoreExceptiontiempo de ejecución.
Sotirios Delimanolis
44
mi pregunta es si las WHYmatrices se hacen covariantes. como mencionó Sotirios, con las matrices se obtendría ArrayStoreException en tiempo de ejecución, si las matrices se hicieran invariables, ¿podríamos detectar este error en tiempo de compilación correcto?
eagertoLearn
@eagertoLearn: Una de las principales debilidades semánticas de Java es que nada en su sistema de tipos puede distinguir "Array que contiene nada más que derivados de Animal, que no tiene que aceptar ningún elemento recibido de otro lugar" de "Array que no debe contener nada más que Animal, y debe estar dispuesto a aceptar referencias proporcionadas externamente Animal. El código que necesita el primero debe aceptar una matriz de Cat, pero el código que necesita el segundo no. Si el compilador podría distinguir los dos tipos, podría proporcionar una verificación en tiempo de compilación. Desafortunadamente, lo único que los distingue ...
supercat
... es si el código realmente intenta almacenar algo en ellos, y no hay forma de saberlo hasta el tiempo de ejecución.
supercat
3

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 uno podría diseñar una Tcolección inmutable que no utilizara a T[]como su almacén de respaldo (por ejemplo, usando un árbol o una lista vinculada), es poco probable que dicha colección funcione tan bien como una respaldada por una matriz. Se podría argumentar que una mejor manera de proporcionar colecciones inmutables covariantes habría sido definir un tipo de "matriz inmutable covariante" en el que podrían usar un almacén de respaldo, pero simplemente permitir la covarianza de la matriz probablemente fuera más fácil.

  • Las matrices serán frecuentemente mutadas por un código que no sabe qué tipo de cosa va a estar en ellas, pero no incluirá en la matriz nada que no haya sido leído de esa 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 contenga una referencia a una matriz y una o más cosas que se había leído de él, y podría incluir métodos para almacenar elementos leídos previamente 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 truco feo, pero en la mayoría de los casos facilita la creación de código de trabajo.

Super gato
fuente
1
The fact that arrays are covariant may be viewed as an ugly hack, but in most cases it facilitates the creation of working code.- buen punto
eagertoLearn
3

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 los tipos comodín:

<E extends Comparable<E>> void sort(E[]);

Para ser realmente útiles, los tipos de comodines requieren 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 se agregaron matrices a Java, y hacer matrices de tipo covariante de referencia permitió una forma mucho más simple de permitir algoritmos polimórficos:

void sort(Comparable[]);

Sin embargo, esa simplicidad abrió un vacío en el sistema de tipo estático:

String[] strings = {"hello"};
Object[] objects = strings;
objects[0] = 1; // throws ArrayStoreException

requiere una verificación de 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 tipos sea más complejo, pero también más seguro de tipo estático, mientras que el enfoque más antiguo era más simple y menos seguro de tipo estático. Los diseñadores del lenguaje optaron por un enfoque más simple, que tenían 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 apremiantes, tenían los recursos para hacerlo bien en genéricos (pero cambiarlo por matrices habría roto los programas Java existentes).

Meriton
fuente
2

Los genéricos son invariantes : de JSL 4.10 :

... El 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 (primer punto):

4.10.3 Subtipado entre tipos de matriz

ingrese la descripción de la imagen aquí

alfasin
fuente
2

Creo que tomaron una decisión equivocada en primer lugar que hizo que la matriz covariante. Rompe el tipo de seguridad como se describe aquí y se quedaron con eso debido a la compatibilidad con versiones anteriores y después de eso trataron de no cometer el mismo error con el genérico. Y esa es una de las razones por las que Joshua Bloch prefiere las listas a las matrices en el ítem 25 del libro "Java efectivo (segunda edición)"

Arnold
fuente
Josh Block fue el autor del marco de colecciones de Java (1.2) y el autor de los genéricos de Java (1.5). Entonces, ¿el tipo que creó los genéricos de los que todos se quejan también es casualmente el tipo que escribió el libro diciendo que son el mejor camino a seguir? No es una gran sorpresa!
cpurdy
1

Mi opinión: cuando el código espera una matriz A [] y le das B [] donde B es una subclase de A, solo hay dos cosas de las que preocuparse: qué sucede cuando lees un elemento de matriz y qué sucede si escribes eso. Por lo tanto, no es difícil escribir reglas de lenguaje para garantizar que se mantenga la seguridad de los tipos en todos los casos (la regla principal es que se ArrayStoreExceptionpodría arrojar un si intentas pegar una A en una B []). Sin embargo, para un genérico, cuando declaras una clase SomeClass<T>, puede haber varias formas de Tusar el cuerpo de la clase, y supongo que es demasiado complicado calcular todas las combinaciones posibles para escribir reglas sobre cuándo las cosas están permitidas y cuando no lo están.

ajb
fuente