instalacion - jre java 32 bits
LambdaConversionException con genéricos: error de JVM? (4)
Tengo un código con una referencia de método que compila bien y falla en tiempo de ejecución.
La excepción es esta:
Caused by: java.lang.invoke.LambdaConversionException: Invalid receiver type class redacted.BasicEntity; not a subtype of implementation type interface redacted.HasImagesEntity
at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:233)
at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303)
at java.lang.invoke.CallSite.makeSite(CallSite.java:289)
La clase que desencadena la excepción:
class ImageController<E extends BasicEntity & HasImagesEntity> {
void doTheThing(E entity) {
Set<String> filenames = entity.getImages().keySet().stream()
.map(entity::filename)
.collect(Collectors.toSet());
}
}
La excepción se produce al intentar resolver
entity::filename
.
filename()
se declara en
HasImagesEntity
.
Por lo que puedo decir, obtengo la excepción porque la
BasicEntity
de E es
BasicEntity
y la JVM no (¿no?) Considera otros límites en E.
Cuando reescribo la referencia del método como una lambda trivial, todo está bien. Me parece realmente sospechoso que una construcción funcione como se esperaba y su equivalente semántico explota.
¿Podría esto posiblemente estar en la especificación? Estoy tratando de encontrar una manera para que esto no sea un problema en el compilador o en el tiempo de ejecución, y no se me ocurrió nada.
Acabo de solucionar este problema en JDK9 y JDK8u45. Ver este error El cambio tardará un poco en filtrarse en las compilaciones promocionadas. Dan acaba de señalarme esta pregunta de , así que agrego esta nota. Cuando encuentre errores, envíelos.
Abordé esto haciendo que el compilador creara un puente, como es el enfoque para muchos casos de referencias de métodos complejos. También estamos examinando las implicaciones de las especificaciones.
Aquí hay un ejemplo simplificado que reproduce el problema y usa solo clases centrales de Java:
public static void main(String[] argv) {
System.out.println(dummy("foo"));
}
static <T extends Serializable&CharSequence> int dummy(T value) {
return Optional.ofNullable(value).map(CharSequence::length).orElse(0);
}
Su suposición es correcta, la implementación específica de JRE recibe el método de destino como
MethodHandle
que no tiene información sobre tipos genéricos.
Por lo tanto, lo único que ve es que los tipos sin formato no coinciden.
Al igual que con muchas construcciones genéricas, se requiere una conversión de tipo en el nivel de código de byte que no aparece en el código fuente.
Dado que
LambdaMetafactory
requiere explícitamente un identificador de método
directo
, una referencia de método que encapsula dicho tipo de
MethodHandle
no se puede pasar como
MethodHandle
a la fábrica.
Hay dos formas posibles de lidiar con eso.
La primera solución sería cambiar la
LambdaMetafactory
para confiar en
MethodHandle
si el tipo de receptor es una
interface
e insertar el tipo de
MethodHandle
requerido por sí mismo en la clase lambda generada en lugar de rechazarlo.
Después de todo, ya es similar para los parámetros y los tipos de retorno.
Alternativamente, el compilador se encargaría de crear un método auxiliar sintético que encapsulara el tipo de conversión y la llamada al método, como si hubiera escrito una expresión lambda.
Esta no es una situación única.
Si usa una referencia de método a un método
varargs
o una creación de matriz como, por ejemplo,
String[]::new
, no se pueden expresar como manejadores de métodos
directos
y terminan en métodos auxiliares sintéticos.
En cualquier caso, podemos considerar el comportamiento actual como un error. Pero obviamente, los compiladores y los desarrolladores de JRE deben acordar de qué manera se debe manejar antes de que podamos decir de qué lado reside el error.
Encontré una solución para esto: cambiar el orden de los genéricos.
Por ejemplo, use la
class A<T extends B & C>
donde necesita acceder a un método
B
, o use la
class A<T extends C & B>
si necesita acceder a un método
C
Por supuesto, si necesita acceso a métodos de ambas clases, esto no funcionará.
Esto me pareció útil cuando una de las interfaces era una interfaz de marcador como
Serializable
.
En cuanto a arreglar esto en el JDK, la única información que pude encontrar fueron algunos errores en el rastreador de errores de openjdk que están marcados como resueltos en la versión 9, lo cual es bastante inútil.
Este error no está completamente solucionado.
Me encontré con una
LambdaConversionException
en 1.8.0_72 y vi que hay informes de errores abiertos en el sistema de seguimiento de errores de Oracle:
link1
,
link2
.
(Editar: se informa que los errores vinculados están cerrados en JDK 9 b93)
Como solución alternativa simple, evito los identificadores de métodos. Entonces en lugar de
.map(entity::filename)
hago
.map(entity -> entity.filename())
Aquí está el código para reproducir el problema en Debian 3.11.8-1 x86_64.
import java.awt.Component;
import java.util.Collection;
import java.util.Collections;
public class MethodHandleTest {
public static void main(String... args) {
new MethodHandleTest().run();
}
private void run() {
ComponentWithSomeMethod myComp = new ComponentWithSomeMethod();
new Caller<ComponentWithSomeMethod>().callSomeMethod(Collections.singletonList(myComp));
}
private interface HasSomeMethod {
void someMethod();
}
static class ComponentWithSomeMethod extends Component implements HasSomeMethod {
@Override
public void someMethod() {
System.out.println("Some method");
}
}
class Caller<T extends Component & HasSomeMethod> {
public void callSomeMethod(Collection<T> components) {
components.forEach(HasSomeMethod::someMethod); // <-- crashes
// components.forEach(comp -> comp.someMethod()); <-- works fine
}
}
}