parameter - La variable de inferencia tiene límites incompatibles. Java 8 ¿Regresión del compilador?
type erasure java (3)
El siguiente programa se compila en Java 7 y en Eclipse Mars RC2 para Java 8:
import java.util.List;
public class Test {
static final void a(Class<? extends List<?>> type) {
b(newList(type));
}
static final <T> List<T> b(List<T> list) {
return list;
}
static final <L extends List<?>> L newList(Class<L> type) {
try {
return type.newInstance();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Usando el compilador javac 1.8.0_45, se reporta el siguiente error de compilación:
Test.java:6: error: method b in class Test cannot be applied to given types;
b(newList(type));
^
required: List<T>
found: CAP#1
reason: inference variable L has incompatible bounds
equality constraints: CAP#2
upper bounds: List<CAP#3>,List<?>
where T,L are type-variables:
T extends Object declared in method <T>b(List<T>)
L extends List<?> declared in method <L>newList(Class<L>)
where CAP#1,CAP#2,CAP#3 are fresh type-variables:
CAP#1 extends List<?> from capture of ? extends List<?>
CAP#2 extends List<?> from capture of ? extends List<?>
CAP#3 extends Object from capture of ?
Una solución es asignar localmente una variable:
import java.util.List;
public class Test {
static final void a(Class<? extends List<?>> type) {
// Workaround here
List<?> variable = newList(type);
b(variable);
}
static final <T> List<T> b(List<T> list) {
return list;
}
static final <L extends List<?>> L newList(Class<L> type) {
try {
return type.newInstance();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
}
Sé que la inferencia de tipos ha cambiado mucho en Java 8 ( por ejemplo, debido a la "inferencia generalizada de tipo de destino" de JEP 101 ). Entonces, ¿es esto un error o una nueva "característica" de lenguaje?
EDITAR : También informé esto a Oracle como JI-9021550, pero en caso de que esta sea una "característica" en Java 8, también informé el problema a Eclipse:
Descargo de responsabilidad: no sé lo suficiente sobre el tema, y el siguiente es un razonamiento mío informal para tratar de justificar el comportamiento de javac.
Podemos reducir el problema a
<X extends List<?>> void a(Class<X> type) throws Exception
{
X instance = type.newInstance();
b(instance); // error
}
<T> List<T> b(List<T> list) { ... }
Para inferir T
tenemos restricciones.
X <: List<?>
X <: List<T>
Esencialmente, esto es irresoluble. Por ejemplo, no existe T
si X=List<?>
.
No estoy seguro de cómo Java7 infiere este caso. Pero javac8 (e IntelliJ) se comporta "razonablemente", diría yo.
Ahora, ¿cómo es que funciona esta solución?
List<?> instance = type.newInstance();
b(instance); // ok!
Funciona debido a la captura de comodines, que introduce más información de tipo, "estrechando" el tipo de instance
instance is List<?> => exist W, where instance is List<W> => T=W
Desafortunadamente, esto no se hace cuando la instance
es X
, por lo tanto, hay menos información de tipo con la que trabajar.
Posiblemente, el lenguaje podría "mejorarse" para hacer la captura de comodines para X también:
instance is X, X is List<?> => exist W, where instance is List<W>
Gracias a la respuesta de bayou.io, podemos reducir el problema al hecho de que
<X extends List<?>> void a(X instance) {
b(instance); // error
}
static final <T> List<T> b(List<T> list) {
return list;
}
produce un error mientras
<X extends List<?>> void a(X instance) {
List<?> instance2=instance;
b(instance2);
}
static final <T> List<T> b(List<T> list) {
return list;
}
Se puede compilar sin problemas. La asignación de instance2=instance
es una conversión de ampliación que también debería ocurrir para los argumentos de invocación de métodos. Entonces, la diferencia con el patrón de esta respuesta es la relación de subtipo adicional.
Tenga en cuenta que aunque no estoy seguro de si este caso específico está en línea con la especificación del lenguaje Java, algunas pruebas revelaron que Eclipse aceptando el código probablemente se deba al hecho de que es más descuidado con respecto a los tipos genéricos en general, como se muestra a continuación: Definitivamente incorrecto, el código podría compilarse sin ningún error ni advertencia:
public static void main(String... arg) {
List<Integer> l1=Arrays.asList(0, 1, 2);
List<String> l2=Arrays.asList("0", "1", "2");
a(Arrays.asList(l1, l2));
}
static final void a(List<? extends List<?>> type) {
test(type);
}
static final <Y,L extends List<Y>> void test(List<L> type) {
L l1=type.get(0), l2=type.get(1);
l2.set(0, l1.get(0));
}
Gracias por el https://bugs.eclipse.org/bugs/show_bug.cgi?id=469297 , y gracias, Holger, por el ejemplo en su respuesta. Estos y muchos otros finalmente me hicieron cuestionar un pequeño cambio realizado en el compilador de Eclipse hace 11 años. El punto era: Eclipse había extendido ilegalmente el algoritmo de captura para aplicar recursivamente a los límites de comodín.
Hubo un ejemplo donde este cambio ilegal alinea perfectamente el comportamiento de Eclipse con javac. Generaciones de desarrolladores de Eclipse han confiado en esta antigua decisión más de lo que podemos ver claramente en JLS. Hoy creo que la desviación previa debe haber tenido una razón diferente.
Hoy tomé el coraje de alinear ecj con JLS en este aspecto y, voila 5 errores que parecían ser extremadamente difíciles de resolver, esencialmente se resolvieron así (más un pequeño retoque aquí y allá como compensación).
Ergo: Sí, Eclipse tenía un error, pero ese error se ha corregido a partir del 4.7 hito 2 :)
Esto es lo que ECJ reportará a partir de ahora:
The method b(List<T>) in the type Test is not applicable for the arguments (capture#1-of ? extends List<?>)
Es el comodín dentro de un límite de captura que no encuentra una regla para detectar la compatibilidad. Más precisamente, en algún momento durante la inferencia (la incorporación para ser precisa) encontramos la siguiente restricción (T # 0 representa una variable de inferencia):
⟨T#0 = ?⟩
De manera ingenua, solo podríamos resolver la variable de tipo al comodín, pero, presumiblemente porque los comodines no se consideran tipos, las reglas de reducción definen lo anterior como reducción a FALSO, lo que permite que la inferencia falle.