java - medicamentos - ¿Cómo funcionan los genéricos de los genéricos?
medicamentos genéricos vs medicamentos de marca (5)
Como Kenny ha notado en su comentario, puedes evitar esto con:
List<Test<? extends Number>> l =
Collections.<Test<? extends Number>>singletonList(t);
Esto nos dice inmediatamente que la operación no es insegura , es solo una víctima de inferencia limitada. Si no fuera seguro, lo anterior no se compilará.
Dado que el uso de parámetros de tipo explícitos en un método genérico como el anterior solo es necesario para actuar como una sugerencia , podemos suponer que lo que se requiere aquí es una limitación técnica del motor de inferencia. De hecho, el compilador de Java 8 está programado actualmente para enviarse con muchas mejoras a la inferencia de tipos . No estoy seguro de si su caso específico será resuelto.
Entonces, ¿qué está pasando realmente?
Bueno, el error de compilación que estamos obteniendo muestra que el parámetro de tipo T
de Collections.singletonList
se infiere como capture<Test<? extends Number>>
capture<Test<? extends Number>>
. En otras palabras, el comodín tiene algunos metadatos asociados que lo vinculan a un contexto específico.
- La mejor manera de pensar en una captura de un comodín (
capture<? extends Foo>
) es como un parámetro de tipo sin nombre de los mismos límites (es decir,<T extends Foo>
, pero sin poder hacer referencia aT
). - La mejor forma de "liberar" el poder de la captura es vinculándolo a un parámetro de tipo nombrado de un método genérico. Lo demostraré en un ejemplo a continuación. Consulte el tutorial de Java "Métodos comodín de captura y ayuda" (gracias por la referencia @WChargin) para leer más.
Digamos que queremos tener un método que cambie una lista, envolviendo la parte posterior. Entonces supongamos que nuestra lista tiene un tipo desconocido (comodín).
public static void main(String... args) {
List<? extends String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
List<? extends String> cycledTwice = cycle(cycle(list));
}
public static <T> List<T> cycle(List<T> list) {
list.add(list.remove(0));
return list;
}
Esto funciona bien, porque T
está decidido a capture<? extends String>
capture<? extends String>
, ? extends String
no ? extends String
? extends String
. Si en cambio usamos esta implementación no genérica del ciclo:
public static List<? extends String> cycle(List<? extends String> list) {
list.add(list.remove(0));
return list;
}
No se podría compilar porque no hemos hecho accesible la captura asignándola a un parámetro de tipo.
Entonces, esto comienza a explicar por qué el consumidor de singletonList
se beneficiaría del type-inferer resolviendo T
to Test<capture<? extends Number>
Test<capture<? extends Number>
y, por lo tanto, devuelve una List<Test<capture<? extends Number>>>
List<Test<capture<? extends Number>>>
lugar de una List<Test<? extends Number>>
List<Test<? extends Number>>
.
Pero, ¿por qué no se puede asignar uno al otro?
¿Por qué no podemos simplemente asignar una List<Test<capture<? extends Number>>>
List<Test<capture<? extends Number>>>
a una List<Test<? extends Number>>
List<Test<? extends Number>>
?
Bueno, si pensamos en el hecho de que la capture<? extends Number>
capture<? extends Number>
es el equivalente de un parámetro de tipo anónimo con un límite superior de Number
, luego podemos convertir esta pregunta en "¿Por qué no se compila lo siguiente?" (¡No lo hace!):
public static <T extends Number> List<Test<? extends Number>> assign(List<Test<T>> t) {
return t;
}
Esto tiene una buena razón para no compilar. Si lo hiciera, entonces esto sería posible:
//all this would be valid
List<Test<Double>> doubleTests = null;
List<Test<? extends Number>> numberTests = assign(doubleTests);
Test<Integer> integerTest = null;
numberTests.add(integerTest); //type error, now doubleTests contains a Test<Integer>
Entonces, ¿por qué funciona ser explícito?
Volvamos al principio. Si lo anterior no es seguro, ¿cómo es posible?
List<Test<? extends Number>> l =
Collections.<Test<? extends Number>>singletonList(t);
Para que esto funcione, implica que lo siguiente está permitido:
Test<capture<? extends Number>> capturedT;
Test<? extends Number> t = capturedT;
Bueno, esta no es una sintaxis válida, ya que no podemos hacer referencia a la captura explícitamente, así que vamos a evaluarla usando la misma técnica que la anterior. Vinculemos la captura a una variante diferente de "asignar":
public static <T extends Number> Test<? extends Number> assign(Test<T> t) {
return t;
}
Esto compila con éxito. Y no es difícil ver por qué debería ser seguro. Es el mismo caso de uso de algo así como
List<? extends Number> l = new List<Double>();
Si bien entiendo algunos de los casos de esquina de genéricos, me falta algo con el siguiente ejemplo.
Tengo la siguiente clase
1 public class Test<T> {
2 public static void main(String[] args) {
3 Test<? extends Number> t = new Test<BigDecimal>();
4 List<Test<? extends Number>> l =Collections.singletonList(t);
5 }
6 }
La línea 4 me da el error
Type mismatch: cannot convert from List<Test<capture#1-of ? extends Number>>
to List<Test<? extends Number>>`.
Obviamente, el compilador piensa que lo diferente ?
no son realmente iguales Mientras que mi instinto me dice, esto es correcto.
¿Alguien puede dar un ejemplo en el que obtendría un error de tiempo de ejecución si la línea 4 fuera legal?
EDITAR:
Para evitar confusiones, reemplacé el =null
en la línea 3 por una asignación concreta
Eche un vistazo al borrado de tipo . El problema es que el "tiempo de compilación" es la única oportunidad que Java tiene para hacer cumplir estos genéricos, por lo que si lo deja pasar no podrá decir si usted intentó insertar algo inválido. Esto es realmente una buena cosa, porque significa que una vez que el programa se compila, los genéricos no incurren en ninguna penalización de rendimiento en tiempo de ejecución.
Probemos y veamos tu ejemplo de otra manera (usemos dos tipos que extienden Número pero se comportan de manera muy diferente). Considere el siguiente programa:
import java.math.BigDecimal;
import java.util.*;
public class q16449799<T extends Number> {
public T val;
public static void main(String ... args) {
q16449799<BigDecimal> t = new q16449799<>();
t.val = new BigDecimal(Math.PI);
List<q16449799<BigDecimal>> l = Collections.singletonList(t);
for(q16449799<BigDecimal> i : l) {
System.out.println(i.val);
}
}
}
Esta salida (como uno esperaría):
3.141592653589793115997963468544185161590576171875
Ahora suponiendo que el código que presentó no causó un error de compilación:
import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicLong;
public class q16449799<T extends Number> {
public T val;
public static void main(String ... args) {
q16449799<BigDecimal> t = new q16449799<>();
t.val = new BigDecimal(Math.PI);
List<q16449799<AtomicLong>> l = Collections.singletonList(t);
for(q16449799<AtomicLong> i : l) {
System.out.println(i.val);
}
}
}
¿Cuál esperarías que fuera la salida? No se puede lanzar razonablemente un BigDecimal a un AtomicLong (se puede construir un AtomicLong a partir del valor de un BigDecimal, pero el fundido y la construcción son cosas diferentes y los genéricos se implementan como azúcar de compilación en tiempo para asegurarse de que los moldes sean exitosos). En cuanto al comentario de @KennyTM, un tipo concreto está buscando en el ejemplo inicial pero intente y compile esto:
import java.math.BigDecimal;
import java.util.*;
public class q16449799<T> {
public T val;
public static void main(String ... args) {
q16449799<? extends Number> t = new q16449799<BigDecimal>();
t.val = new BigDecimal(Math.PI);
List<q16449799<? extends Number>> l = Collections.<q16449799<? extends Number>>singletonList(t);
for(q16449799<? extends Number> i : l) {
System.out.println(i.val);
}
}
}
Esto generará un error en el momento en que intente y establezca un valor para t.val
.
La razón es que el compilador no sabe que los tipos de comodines son del mismo tipo.
Tampoco sabe que su instancia es null
. Aunque null
es un miembro de todos los tipos, el compilador considera solo los tipos declarados , no lo que el valor de la variable puede contener, cuando se comprueba el tipo.
Si el código se ejecuta, no causaría una excepción, pero eso es solo porque el valor es nulo. Todavía existe un posible desajuste de tipo, y ese es el trabajo del compilador: no permitir las discrepancias de tipo.
No hay un posible error de tiempo de ejecución, está fuera de la capacidad del compilador para determinar esto de forma estática. Siempre que cause una inferencia de tipo, automáticamente genera una nueva captura de <? extends Number>
<? extends Number>
, y dos capturas no se consideran equivalentes.
Por lo tanto, si elimina la inferencia de la invocación de singletonList especificando <T>
para ello:
List<Test<? extends Number>> l = Collections.<Test<? extends Number>>singletonList(t);
Funciona bien. El código generado no es diferente de que si su llamada fue legal, es solo una limitación del compilador que no puede resolverlo por sí mismo.
La regla de que una inferencia crea una captura y captura no es compatible es lo que impide que este ejemplo de tutorial se compile y explote en tiempo de ejecución:
public static void swap(List<? extends Number> l1, List<? extends Number> l2) {
Number num = l1.get(0);
l1.add(0, l2.get(0));
l2.add(0, num);
}
Sí, es probable que la especificación del lenguaje y el compilador sean más sofisticados para diferenciar su ejemplo de eso, pero no es así y es lo suficientemente simple para evitarlo.
Quizás esto pueda explicar el problema del compilador:
List<? extends Number> myNums = new ArrayList<Integer>();
Esta lista de comodines genéricos puede contener cualquier elemento que se extienda desde Número. Así que está bien asignarle una lista de enteros. Sin embargo, ahora podría agregar un doble a myNums porque el doble también se está extendiendo desde el número, lo que daría lugar a un problema de tiempo de ejecución. Así que el compilador prohíbe todo acceso de escritura a myNums y solo puedo usar métodos de lectura en él, porque solo sé que lo que obtenga se puede convertir en Number.
Y entonces el compilador se queja de muchas cosas que puede hacer con un comodín genérico. A veces él está enojado con cosas que puede asegurar que están seguros y bien.
Pero afortunadamente hay un truco para evitar este error, así que puedes probar por tu cuenta lo que puede romper esto:
public static void main(String[] args) {
List<? extends Number> list1 = new ArrayList<BigDecimal>();
List<List<? extends Number>> list2 = copyHelper(list1);
}
private static <T> List<List<T>> copyHelper(List<T> list) {
return Collections.singletonList(list);
}