programacion - Múltiples comodines en un método genérico hace que el compilador de Java(¡y yo!) Esté muy confundido
que es jvm en programacion (3)
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<String> list1 = null;
List<Integer> list2 = null;
doNothing(list1, list2); // compiles fine!
}
}
Los dos comodines no están relacionados, por lo que puede llamar a doNothing
con una List<String>
y una List<Integer>
. En otras palabras, ¿los dos ?
puede referirse a tipos completamente diferentes. Por lo tanto, no se compila 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<? extends capture#1-of ?>)
// in the type List<capture#1-of ?> is not applicable for
// the arguments (List<capture#2-of ?>)
}
}
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 compila 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<Integer>> lol
y una List<String> 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 compila, como es de esperar ( como se ve en ideone.com ):
import java.util.*;
public class LOLUnknowns2 {
static void rightfullyIllegal(
List<List<? extends Number>> lol, List<?> list) {
lol.add(list); // DOES NOT COMPILE! As expected!!!
// The method add(List<? extends Number>) in the type
// List<List<? extends Number>> is not applicable for
// the arguments (List<capture#1-of ?>)
}
}
Entonces parece que el compilador 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<? extends Number>> lol, List<? extends Number> list) {
lol.add(list); // compiles fine!!! how come???
}
}
De nuevo, podemos tener, por ejemplo, una List<List<Integer>> lol
y una List<Float> list
, por lo que no debería compilarse, ¿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<String>> lol = null;
List<String> 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<String>>, List<String>)
}
}
¡Esto no tiene sentido! ¡Aquí ni siquiera estamos tratando de usar dos tipos diferentes, y no compila! Hacerlo una List<List<Integer>> lol
y List<String> list
también da un error de compilación similar! De hecho, de mi experimentación, la única forma en que el código se compila 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<String> 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);
compilar en absoluto? ¿Es seguro? - ¿Es esto un error del compilador 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 compila 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 anidados: ¿qué significan realmente?
Investigaciones adicionales indican que quizás varios comodines no tienen nada que ver con el problema, sino que un comodín anidado es la fuente de la confusión.
import java.util.*;
public class IntoTheWild {
public static void main(String[] args) {
List<?> list = new ArrayList<String>(); // compiles fine!
List<List<?>> lol = new ArrayList<List<String>>(); // DOES NOT COMPILE!!!
// Type mismatch: cannot convert from
// ArrayList<List<String>> to List<List<?>>
}
}
Entonces parece que una List<List<String>>
no es una List<List<?>>
. De hecho, aunque cualquier List<E>
es una List<?>
, No parece que ninguna List<List<E>>
sea una List<List<?>>
( como se ve en ideone.com ):
import java.util.*;
public class IntoTheWild2 {
static <E> List<?> makeItWild(List<E> list) {
return list; // compiles fine!
}
static <E> List<List<?>> makeItWildLOL(List<List<E>> lol) {
return lol; // DOES NOT COMPILE!!!
// Type mismatch: cannot convert from
// List<List<E>> 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<List<?>>
.
Primero recordemos qué significa que los genéricos de Java son invariables:
- Un número
Integer
es unNumber
- Una
List<Integer>
NO es unaList<Number>
- Una
List<Integer>
IS aList<? 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) :
- Una
List<String>
es (capturable por) unaList<?>
- Una
List<List<String>>
NO es (capturable por) aList<List<?>>
- Una
List<List<String>>
IS (capturable por) aList<? 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<List<?>>
puede capturar tipos como List<List<String>>
, List<List<Integer>>
, etc. Esto NO es verdadero.
Es decir, una List<List<?>>
:
- NO es una lista cuyos elementos son listas de algún tipo desconocido.
- ... eso sería una
List<? extends List<?>>
List<? extends List<?>>
- ... eso sería una
- En cambio, es una lista cuyos elementos son listas de CUALQUIER tipo.
Fragmentos
Aquí hay un fragmento para ilustrar los puntos anteriores:
List<List<?>> lolAny = new ArrayList<List<?>>();
lolAny.add(new ArrayList<Integer>());
lolAny.add(new ArrayList<String>());
// lolAny = new ArrayList<List<String>>(); // DOES NOT COMPILE!!
List<? extends List<?>> lolSome;
lolSome = new ArrayList<List<String>>();
lolSome = new ArrayList<List<Integer>>();
Más fragmentos
Aquí hay otro ejemplo con un comodín anidado limitado:
List<List<? extends Number>> lolAnyNum = new ArrayList<List<? extends Number>>();
lolAnyNum.add(new ArrayList<Integer>());
lolAnyNum.add(new ArrayList<Float>());
// lolAnyNum.add(new ArrayList<String>()); // DOES NOT COMPILE!!
// lolAnyNum = new ArrayList<List<Integer>>(); // DOES NOT COMPILE!!
List<? extends List<? extends Number>> lolSomeNum;
lolSomeNum = new ArrayList<List<Integer>>();
lolSomeNum = new ArrayList<List<Float>>();
// lolSomeNum = new ArrayList<List<String>>(); // 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<capture#1-of ? extends List<?>> is not
// applicable for the arguments (List<capture#3-of ?>)
}
public static void main(String[] args) {
List<Object> list = null;
List<List<String>> lolString = null;
List<List<Integer>> lolInteger = null;
// these casts are valid
nowDefinitelyIllegal(lolString, list);
nowDefinitelyIllegal(lolInteger, list);
}
}
lol.add(list);
es ilegal porque podemos tener una List<List<String>> lol
y una List<Object> list
. De hecho, si comentamos la declaración ofensiva, el código compila 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 compilador. Está haciendo exactamente lo que se supone que debe hacer.
Referencias
Preguntas relacionadas
- ¿Alguna forma sencilla de explicar por qué no puedo hacer una
List<Animal> animals = new ArrayList<Dog>()
? - Java genérico anidado genérico no compilará
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 declaración 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 <T 1 ... T n > a G <S 1 ... S n > , donde, para 1 <= i <= n :
- Si T i es un argumento de tipo comodín de la forma
?
entonces …- Si T i es un argumento de tipo comodín de la forma
? extends
? extends
B i , entonces ...- Si T i es un argumento de tipo comodín de la forma
? super
? super
B i , entonces ...- 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 CCList<String>
- El
?
puede CC por la regla 1
- El
-
List<? extends Number>
List<? extends Number>
puede CCList<Integer>
- El
?
puede CC por la regla 2 - Al aplicar la regla 2, B i es simplemente
Number
- El
-
List<? extends Number>
List<? extends Number>
no puede CCList<String>
- El
?
puede CC por la regla 2, pero se produce un error de tiempo de compilación debido a tipos incompatibles
- El
Ahora intentemos anidar:
-
List<List<?>>
no puede CCList<List<String>>
- Se aplica la regla 4, y CC no es recursiva, ¿entonces
?
NO se puede CC
- Se aplica la regla 4, y CC no es recursiva, ¿entonces
-
List<? extends List<?>>
List<? extends List<?>>
puede CCList<List<String>>
- El primero
?
puede CC por la regla 2 - Al aplicar la regla 2, B i ahora es una
List<?>
, Que puede CCList<String>
-
?
Ambos?
puede CC
- El primero
-
List<? extends List<? extends Number>>
List<? extends List<? extends Number>>
puedeList<List<Integer>>
CCList<List<Integer>>
- 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 CCList<Integer>
-
?
Ambos?
puede CC
- El primero
-
List<? extends List<? extends Number>>
List<? extends List<? extends Number>>
no puedeList<List<Integer>>
CCList<List<Integer>>
- 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 compilación cuando se aplica aList<Integer>
-
?
Ambos?
puede CC
- El primero
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 compilación:
// WildSnippet1
new HashMap<?,?>(); // DOES NOT COMPILE!!!
new HashMap<List<?>, ?>(); // DOES NOT COMPILE!!!
new HashMap<?, Set<?>>(); // DOES NOT COMPILE!!!
Sin embargo, lo siguiente compila muy bien:
// WildSnippet2
new HashMap<List<?>,Set<?>>(); // compiles fine!
new HashMap<Map<?,?>, Map<?,Map<?,?>>>(); // compiles fine!
La razón por la cual WildSnippet2
compila es porque, como se explicó anteriormente, ¿ninguno de los ?
puede CC. En WildSnippet1
, la K
o la V
(o ambas) de HashMap<K,V>
pueden CC, lo que hace que la instanciación directa a través de new
ilegal.
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<Class<?>> 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<String>
y una Class<Int>
. esto no se puede expresar con un parámetro de tipo ordinario:
static<T> void probablyIllegal(Class<T>[] x //homogeneous! not the same!
Class<?>
Es un súper tipo de Class<T>
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<T>
para todos los T
(¿Incluye itselft? No lo sé ...)
No se debe aceptar ningún argumento con genéricos . En el caso de
LOLUnknowns1b
elnull
se acepta como si el primer argumento se escribiera comoList
. Por ejemplo, esto sí compila:List lol = null; List<String> list = null; probablyIllegal(lol, list);
IMHO
lol.add(list);
ni siquiera debería compilar, pero comolol.add()
necesita un argumento de tipoList<?>
y como la lista cabe enList<?>
, funciona.
Un extraño ejemplo que me hace pensar en esta teoría es:static void probablyIllegalAgain(List<List<? extends Number>> lol, List<? extends Integer> list) { lol.add(list); // compiles fine!!! how come??? }
lol.add()
necesita un argumento de tipoList<? extends Number>
List<? extends Number>
y la lista se escribe comoList<? extends Integer>
List<? extends Integer>
, encaja. No funcionará si no coincide. Lo mismo para el doble LOL y otros comodines anidados, 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 genéricos
EDITs:
- Se agregó un comentario sobre Double Lol
- Y comodines anidados.