Genéricos de Java: ¿No se puede convertir la Lista a la Lista ?

Solo dame con este problema:

List a1 = new ArrayList(); List b1 = a1; // compile error: incompatible type 

Donde el tipo DataNode es un subtipo de Tree.

 public class DataNode implements Tree 

Para mi sorpresa, esto funciona para array:

 DataNode[] a2 = new DataNode[0]; Tree[] b2 = a2; // this is okay 

Esto me gusta un poco extraño. ¿Alguien puede dar una explicación sobre esto?

Lo que estás viendo en el segundo caso es la matriz de covarianzas . Es algo malo que IMO haga las asignaciones dentro de la matriz inseguras: pueden fallar en el momento de la ejecución, a pesar de estar bien en el momento de la comstackción.

En el primer caso, imagine que el código se compiló y fue seguido por:

 b1.add(new SomeOtherTree()); DataNode node = a1.get(0); 

¿Qué esperarías que sucediera?

Puedes hacerlo:

 List a1 = new ArrayList(); List b1 = a1; 

… porque entonces solo puedes obtener cosas de b1 , y se garantiza que son compatibles con Tree . No puede llamar a b1.add(...) precisamente porque el comstackdor no sabrá si es seguro o no.

Eche un vistazo a esta sección de Preguntas frecuentes sobre generics Java de Angelika Langer para obtener más información.

La breve explicación: fue un error permitirlo originalmente para Arrays.

La explicación más larga:

Supongamos que esto estuviera permitido:

 List a1 = new ArrayList(); List b1 = a1; // pretend this is allowed 

Entonces no podría proceder a:

 b1.add(new TreeThatIsntADataNode()); // Hey, b1 is a List, so this is fine for (DataNode dn : a1) { // Uh-oh! There's stuff in a1 that isn't a DataNode!! } 

Ahora, una solución ideal permitiría el tipo de conversión que desee al usar una variante de la List que fuera de solo lectura, pero no lo permitiría cuando use una interfaz (como la List ) que sea de lectura-escritura. Java no permite ese tipo de notación de varianza en los parámetros generics, (*) pero incluso si lo hiciera, no podría convertir una List en una List menos que A y B fueran idénticos.

(*) Es decir, no lo permite al escribir clases. Puede declarar que su variable tiene el tipo List List , y eso está bien.

Si tiene que convertir de List a List , y sabe que es seguro hacerlo, entonces una manera fea de lograr esto es hacer un doble cast:

List a1 = new ArrayList();

List b1 = (List) (List) a1;

List no extiende List aunque DataNode extienda Tree . Esto se debe a que después de su código puede hacer b1.add (SomeTreeThatsNotADataNode), y eso sería un problema ya que entonces a1 también tendría un elemento que no es un DataNode.

Necesita usar un comodín para lograr algo como esto

 List a1 = new ArrayList(); List b1 = a1; b1.add(new Tree()); // compiler error, instead of runtime error 

Por otro lado, DataNode[] DOES extiende Tree[] . En ese momento parecía lo lógico, pero puedes hacer algo como:

 DataNode[] a2 = new DataNode[1]; Tree[] b2 = a2; // this is okay b2[0] = new Tree(); // this will cause ArrayStoreException since b2 is actually a DataNode[] and can't store a Tree 

Es por eso que cuando agregaron generics a colecciones, eligieron hacerlo de forma un poco diferente para evitar errores de tiempo de ejecución.

Cuando las matrices se diseñaron (es decir, más o menos cuando se diseñó Java), los desarrolladores decidieron que la variación sería útil, por lo que lo permitieron. Sin embargo, esta decisión a menudo fue criticada porque le permite hacer esto (suponiendo que NotADataNode es otra subclase de Tree ):

 DataNode[] a2 = new DataNode[1]; Tree[] b2 = a2; // this is okay b2[0] = new NotADataNode(); //compiles fine, causes runtime error 

Entonces, cuando se diseñaron los generics, se decidió que las estructuras de datos generics solo deberían permitir la varianza explícita. Es decir, no puede hacer la List b1 = a1; , pero puedes hacer la List b1 = a1; List b1 = a1; .

Sin embargo, si hace esto último, tratar de usar el método add o set (o cualquier otro método que tome una T como argumento) causará un error de comstackción. De esta forma, no es posible comstackr el equivalente del problema de la matriz anterior (sin moldes inseguros).

Respuesta corta: la lista a1 no es del mismo tipo que la lista b2; En a1 puedes poner cualquier tipo de objeto que sea extenso DataNode. Por lo tanto, puede contener otros tipos distintos a Tree.

Es la respuesta de C #, pero creo que en realidad no importa aquí, ya que la razón es la misma.

“En particular, a diferencia de los tipos de matriz, los tipos de referencia construidos no exhiben conversiones” covariantes “. Esto significa que un tipo Lista no tiene conversión (implícita o explícita) a la Lista incluso si B se deriva de A. Del mismo modo, no existe una conversión de List a List .

La razón de esto es simple: si se permite una conversión a List , entonces aparentemente se pueden almacenar valores de tipo A en la lista. Pero esto rompería la invariante de que cada objeto en una lista de tipo Lista siempre es un valor de tipo B, o pueden ocurrir fallas inesperadas cuando se asignan a clases de colección. ”

http://social.msdn.microsoft.com/forums/en-US/clr/thread/22e262ed-c3f8-40ed-baf3-2cbcc54a216e

DataNode podría ser un subtipo de Tree, pero List DataNode no es un subtipo de List Tree.

https://docs.oracle.com/javase/tutorial/extra/generics/subtype.html

Este es un problema clásico con generics implementados con borrado de tipo.

Supongamos que su primer ejemplo realmente funcionó. Entonces, podrías hacer lo siguiente:

 List a1 = new ArrayList(); List b1 = a1; // suppose this works b1.add(new Tree()); 

Pero dado que b1 y a1 refieren al mismo objeto, significa que a1 ahora se refiere a una List que contiene tanto DataNode como Tree . Si intenta obtener ese último elemento, obtendrá una excepción (no recuerdo cuál).

Bueno, voy a ser honesto aquí: la implementación de la genericidad vaga.

No hay razón semántica para no permitir su primera afectación.

A propósito, aunque me encantaba la creación de plantillas en C ++, los generics, junto con el tipo de limitación tonta que tenemos aquí, son la razón principal por la que renuncié a Java.