Múltiples comodines en un método genérico hace que el comstackdor de Java (¡y yo!) Esté muy confundido

Primero consideremos un escenario simple ( vea la fuente completa en ideone.com ):

import java.util.*; public class TwoListsOfUnknowns { static void doNothing(List list1, List list2) { } public static void main(String[] args) { List list1 = null; List list2 = null; doNothing(list1, list2); // compiles fine! } } 

Los dos comodines no están relacionados, por lo que puede llamar a doNothing con una List y una List . En otras palabras, ¿los dos ? puede referirse a tipos completamente diferentes. Por lo tanto, no se comstack lo siguiente, como es de esperar ( también en ideone.com ):

 import java.util.*; public class TwoListsOfUnknowns2 { static void doSomethingIllegal(List list1, List list2) { list1.addAll(list2); // DOES NOT COMPILE!!! // The method addAll(Collection) // in the type List is not applicable for // the arguments (List) } } 

Hasta aquí todo bien, pero aquí es donde las cosas empiezan a ser muy confusas ( como se ve en ideone.com ):

 import java.util.*; public class LOLUnknowns1 { static void probablyIllegal(List<List> lol, List list) { lol.add(list); // this compiles!! how come??? } } 

El código anterior comstack para mí en Eclipse y en sun-jdk-1.6.0.17 en ideone.com, pero ¿o sí? ¿No es posible que tengamos una List<List> lol y una List list , las dos situaciones análogas de wildcards no relacionadas de TwoListsOfUnknowns ?

De hecho, la siguiente ligera modificación hacia esa dirección no se comstack, como es de esperar ( como se ve en ideone.com ):

 import java.util.*; public class LOLUnknowns2 { static void rightfullyIllegal( List<List> lol, List list) { lol.add(list); // DOES NOT COMPILE! As expected!!! // The method add(List) in the type // List<List> is not applicable for // the arguments (List) } } 

Entonces parece que el comstackdor está haciendo su trabajo, pero luego obtenemos esto ( como se ve en ideone.com ):

 import java.util.*; public class LOLUnknowns3 { static void probablyIllegalAgain( List<List> lol, List list) { lol.add(list); // compiles fine!!! how come??? } } 

De nuevo, podemos tener, por ejemplo, una List<List> lol y una List list , por lo que no debería comstackrse, ¿no?

De hecho, volvamos al LOLUnknowns1 (dos comodines ilimitados) más simple e intentamos ver si de hecho podemos invocar probablyIllegal Ilegal de alguna manera. Primero probemos el caso “fácil” y elijamos el mismo tipo para los dos comodines ( como se ve en ideone.com ):

 import java.util.*; public class LOLUnknowns1a { static void probablyIllegal(List<List> lol, List list) { lol.add(list); // this compiles!! how come??? } public static void main(String[] args) { List<List> lol = null; List list = null; probablyIllegal(lol, list); // DOES NOT COMPILE!! // The method probablyIllegal(List<List>, List) // in the type LOLUnknowns1a is not applicable for the // arguments (List<List>, List) } } 

¡Esto no tiene sentido! ¡Aquí ni siquiera estamos tratando de usar dos tipos diferentes, y no comstack! Hacerlo una List<List> lol y List list también da un error de comstackción similar! De hecho, de mi experimentación, la única forma en que el código se comstack es si el primer argumento es un tipo null explícito ( como se ve en ideone.com ):

 import java.util.*; public class LOLUnknowns1b { static void probablyIllegal(List<List> lol, List list) { lol.add(list); // this compiles!! how come??? } public static void main(String[] args) { List list = null; probablyIllegal(null, list); // compiles fine! // throws NullPointerException at run-time } } 

Entonces las preguntas son, con respecto a LOLUnknowns1 , LOLUnknowns1a y LOLUnknowns1b :

  • ¿Qué tipos de argumentos acepta probablyIllegal Illegal?
  • Debería lol.add(list); comstackr en absoluto? ¿Es seguro?
  • ¿Es esto un error del comstackdor o estoy malinterpretando las reglas de conversión de captura para comodines?

Apéndice A: ¿Doble jaja?

En caso de que alguien tenga curiosidad, esto comstack bien ( como se ve en ideone.com ):

 import java.util.*; public class DoubleLOL { static void omg2xLOL(List<List> lol1, List<List> lol2) { // compiles just fine!!! lol1.addAll(lol2); lol2.addAll(lol1); } } 

Apéndice B: comodines nesteds: ¿qué significan realmente?

Investigaciones adicionales indican que quizás varios comodines no tienen nada que ver con el problema, sino que un comodín nested es la fuente de la confusión.

 import java.util.*; public class IntoTheWild { public static void main(String[] args) { List list = new ArrayList(); // compiles fine! List<List> lol = new ArrayList<List>(); // DOES NOT COMPILE!!! // Type mismatch: cannot convert from // ArrayList<List> to List<List> } } 

Entonces parece que una List<List> no es una List<List> . De hecho, aunque cualquier List es una List , No parece que ninguna List<List> sea ​​una List<List> ( como se ve en ideone.com ):

 import java.util.*; public class IntoTheWild2 { static  List makeItWild(List list) { return list; // compiles fine! } static  List<List> makeItWildLOL(List<List> lol) { return lol; // DOES NOT COMPILE!!! // Type mismatch: cannot convert from // List<List> to List<List> } } 

Surge una nueva pregunta, entonces: ¿qué es una List<List> ?

Como lo indica el Apéndice B, esto no tiene nada que ver con varios comodines, sino más bien, sin entender lo que realmente significa List> .

Primero recordemos qué significa que los generics de Java son invariables:

  1. Un número Integer es un Number
  2. Una List NO es una List
  3. Una List IS a List< ? extends Number> List< ? extends Number>

Ahora simplemente aplicamos el mismo argumento a nuestra situación de lista anidada (ver el apéndice para más detalles) :

  1. Una List es (capturable por) una List< ?>
  2. Una List> NO es (capturable por) a List>
  3. Una List> IS (capturable por) a List< ? extends List> List< ? extends List>

Con este entendimiento, se pueden explicar todos los fragmentos en la pregunta. La confusión surge al creer (falsamente) que un tipo como List> puede capturar tipos como List> , List> , etc. Esto NO es verdadero.

Es decir, una List> :

  • NO es una lista cuyos elementos son listas de algún tipo desconocido.
    • … eso sería una List< ? extends List> List< ? extends List>
  • En cambio, es una lista cuyos elementos son listas de CUALQUIER tipo.

Fragmentos

Aquí hay un fragmento para ilustrar los puntos anteriores:

 List> lolAny = new ArrayList>(); lolAny.add(new ArrayList()); lolAny.add(new ArrayList()); // lolAny = new ArrayList>(); // DOES NOT COMPILE!! List< ? extends List> lolSome; lolSome = new ArrayList>(); lolSome = new ArrayList>(); 

Más fragmentos

Aquí hay otro ejemplo con un comodín nested limitado:

 List> lolAnyNum = new ArrayList>(); lolAnyNum.add(new ArrayList()); lolAnyNum.add(new ArrayList()); // lolAnyNum.add(new ArrayList()); // DOES NOT COMPILE!! // lolAnyNum = new ArrayList>(); // DOES NOT COMPILE!! List< ? extends List> lolSomeNum; lolSomeNum = new ArrayList>(); lolSomeNum = new ArrayList>(); // lolSomeNum = new ArrayList>(); // DOES NOT COMPILE!! 

Volver a la pregunta

Para volver a los fragmentos en la pregunta, el siguiente comportamiento se comporta como se esperaba ( como se ve en ideone.com ):

 public class LOLUnknowns1d { static void nowDefinitelyIllegal(List< ? extends List> lol, List< ?> list) { lol.add(list); // DOES NOT COMPILE!!! // The method add(capture#1-of ? extends List< ?>) in the // type List> is not // applicable for the arguments (List) } public static void main(String[] args) { List list = null; List> lolString = null; List> lolInteger = null; // these casts are valid nowDefinitelyIllegal(lolString, list); nowDefinitelyIllegal(lolInteger, list); } } 

lol.add(list); es ilegal porque podemos tener una List> lol y una Listlist . De hecho, si comentamos la statement ofensiva, el código comstack y eso es exactamente lo que tenemos con la primera invocación en main .

Todos los métodos ilegales probablyIllegal en la pregunta no son ilegales. Todos son perfectamente legales y seguros. No hay absolutamente ningún error en el comstackdor. Está haciendo exactamente lo que se supone que debe hacer.


Referencias

  • Preguntas frecuentes sobre generics Java de Angelika Langer
    • ¿Qué relaciones de super-subtipo existen entre las instancias de tipos generics?
    • ¿Puedo crear un objeto cuyo tipo es un tipo de comodín con parámetros?
  • JLS 5.1.10 Conversión de captura

Preguntas relacionadas

  • ¿Alguna forma sencilla de explicar por qué no puedo hacer una List animals = new ArrayList() ?
  • Java genérico nested genérico no comstackrá

Apéndice: las reglas de conversión de captura

(Esto se mencionó en la primera revisión de la respuesta, es un complemento digno del argumento de tipo invariante).

5.1.10 Conversión de captura

Deje que G nombre una statement de tipo genérico con n parámetros de tipo formales A 1 … A n con los límites correspondientes U 1 … U n . Existe una conversión de captura de G 1 … T n > a G 1 … S n > , donde, para 1 < = i <= n :

  1. Si T i es un argumento de tipo comodín de la forma ? entonces …
  2. Si T i es un argumento de tipo comodín de la forma ? extends ? extends B i , entonces …
  3. Si T i es un argumento de tipo comodín de la forma ? super ? super B i , entonces …
  4. De lo contrario, S i = T i .

La conversión de captura no se aplica recursivamente.

Esta sección puede ser confusa, especialmente con respecto a la aplicación no recursiva de la conversión de captura (en adelante, CC ), pero la clave es que no todos ? puede CC; depende de dónde aparezca . No existe una aplicación recursiva en la regla 4, pero cuando se aplican las reglas 2 o 3, entonces la B i respectiva puede ser el resultado de una CC.

Analicemos algunos ejemplos simples:

  • List< ?> Puede CC List
    • El ? puede CC por la regla 1
  • List< ? extends Number> List< ? extends Number> puede CC List
    • El ? puede CC por la regla 2
    • Al aplicar la regla 2, B i es simplemente Number
  • List< ? extends Number> List< ? extends Number> no puede CC List
    • El ? puede CC por la regla 2, pero se produce un error de tiempo de comstackción debido a tipos incompatibles

Ahora intentemos anidar:

  • List> no puede CC List>
    • Se aplica la regla 4, y CC no es recursiva, ¿entonces ? NO se puede CC
  • List< ? extends List> List< ? extends List> puede CC List>
    • El primero ? puede CC por la regla 2
    • Al aplicar la regla 2, B i ahora es una List< ?> , Que puede CC List
    • ? Ambos ? puede CC
  • List< ? extends List> List< ? extends List> puede List> CC List>
    • El primero ? puede CC por la regla 2
    • Al aplicar la regla 2, B i ahora es una List< ? extends Number> List< ? extends Number> , que puede CC List
    • ? Ambos ? puede CC
  • List< ? extends List> List< ? extends List> no puede List> CC List>
    • El primero ? puede CC por la regla 2
    • Al aplicar la regla 2, B i ahora es una List< ? extends Number> List< ? extends Number> , que puede CC, pero da un error de tiempo de comstackción cuando se aplica a List
    • ? Ambos ? puede CC

Para ilustrar aún más por qué algunos ? CC y otros no pueden, considere la siguiente regla: NO puede crear instancias directas de un tipo de comodín. Es decir, lo siguiente da un error de tiempo de comstackción:

  // WildSnippet1 new HashMap< ?,?>(); // DOES NOT COMPILE!!! new HashMap, ?>(); // DOES NOT COMPILE!!! new HashMap< ?, Set>(); // DOES NOT COMPILE!!! 

Sin embargo, lo siguiente comstack muy bien:

  // WildSnippet2 new HashMap,Set< ?>>(); // compiles fine! new HashMap, Map< ?,Map>>(); // compiles fine! 

La razón por la cual WildSnippet2 comstack es porque, como se explicó anteriormente, ¿ninguno de los ? puede CC. En WildSnippet1 , la K o la V (o ambas) de HashMap pueden CC, lo que hace que la instanciación directa a través de new ilegal.

  • No se debe aceptar ningún argumento con generics . En el caso de LOLUnknowns1b el null se acepta como si el primer argumento se escribiera como List . Por ejemplo, esto sí comstack:

     List lol = null; List list = null; probablyIllegal(lol, list); 
  • IMHO lol.add(list); ni siquiera debería comstackr, pero como lol.add() necesita un argumento de tipo List< ?> y como la lista cabe en List< ?> , funciona.
    Un extraño ejemplo que me hace pensar en esta teoría es:

     static void probablyIllegalAgain(List> lol, List< ? extends Integer> list) { lol.add(list); // compiles fine!!! how come??? } 

    lol.add() necesita un argumento de tipo List< ? extends Number> List< ? extends Number> y la lista se escribe como List< ? extends Integer> List< ? extends Integer> , encaja. No funcionará si no coincide. Lo mismo para el doble LOL y otros comodines nesteds, siempre que la primera captura coincida con la segunda, todo está bien (y no debería ser).

  • De nuevo, no estoy seguro, pero realmente parece un error.

  • Me alegra no ser el único en usar variables de lol todo el tiempo.

Recursos:
http://www.angelikalanger.com , preguntas frecuentes sobre generics

EDITs:

  1. Se agregó un comentario sobre Double Lol
  2. Y comodines nesteds.

no es un experto, pero creo que puedo entenderlo.

cambiemos su ejemplo a algo equivalente, pero con más tipos distintivos:

 static void probablyIllegal(List> x, Class< ?> y) { x.add(y); // this compiles!! how come??? } 

cambiemos la Lista a [] para que sea más esclarecedor:

 static void probablyIllegal(Class< ?>[] x, Class< ?> y) { x.add(y); // this compiles!! how come??? } 

ahora, x no es una matriz de algún tipo de clase. es una matriz de cualquier tipo de clase. puede contener una Class y una Class . esto no se puede express con un parámetro de tipo ordinario:

 static void probablyIllegal(Class[] x //homogeneous! not the same! 

Class< ?> Es un súper tipo de Class para cualquier T Si pensamos que un tipo es un conjunto de objetos , establecemos que Class< ?> Es la unión de todos los conjuntos de Class para todos los T (¿Incluye itselft? No lo sé …)