LambdaConversionException con generics: ¿error de JVM?

Tengo un código con una referencia de método que comstack bien y falla en tiempo de ejecución.

La excepción es así:

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 es así:

 class ImageController { void doTheThing(E entity) { Set filenames = entity.getImages().keySet().stream() .map(entity::filename) .collect(Collectors.toSet()); } } 

La excepción se produce tratando de resolver entidad :: nombre de archivo. filename () se declara en HasImagesEntity. Cerca de lo que puedo decir, recibo la excepción porque el borrado de E es BasicEntity y la JVM no (¿no puede?) Considerar 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 espera y su equivalente semántico explote. ¿Podría esto posiblemente estar en la especificación? Intento mucho encontrar una forma de que esto no sea un problema en el comstackdor o en el tiempo de ejecución, y no he encontrado nada.

Aquí hay un ejemplo simplificado que reproduce el problema y utiliza solo clases Java centrales:

 public static void main(String[] argv) { System.out.println(dummy("foo")); } static  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 generics. Por lo tanto, lo único que ve es que los tipos crudos no coinciden.

Al igual que con muchas construcciones genéricas, se requiere un tipo de conversión en el nivel de código de bytes que no aparece en el código fuente. Como 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 un MethodHandle a la fábrica.

Hay dos formas posibles de manejarlo.

La primera solución sería cambiar la LambdaMetafactory para confiar en el 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 rechazarla. Después de todo, ya hace similares para los tipos de parámetros y retorno.

Alternativamente, el comstackdor se encargaría de crear un método auxiliar sintético que encapsule 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 utiliza una referencia de método a un método varargs o una creación de matriz como, por ejemplo, String[]::new , no se pueden express como identificadores directos de métodos y terminar en métodos auxiliares sintéticos.

En cualquier caso, podemos considerar el comportamiento actual como un error. Pero, obviamente, los desarrolladores de comstackdores y JRE deben acordar de qué manera se debe manejar antes de que podamos decir de qué lado reside el error.

Acabo de solucionar este problema en JDK9 y JDK8u45. Mira este error . El cambio tardará un poco en filtrarse en versiones promocionadas. Dan solo me señaló esta pregunta de StackOverflow, así que estoy agregando esta nota. Cuando encuentre errores, por favor envíelos.

Abordé esto haciendo que el comstackdor creara un puente, como es el enfoque para muchos casos de referencias a métodos complejos. También estamos examinando las implicaciones de las especificaciones.

Este error no está completamente solucionado. Acabo de encontrarme con una LambdaConversionException en 1.8.0_72 y vi que hay informes de errores abiertos en el sistema de seguimiento de fallas de Oracle: link1 , link2 .

(Editar: se informa que los errores vinculados se cierran en JDK 9 b93)

Como una solución simple evito los manejadores de método. 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().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 { public void callSomeMethod(Collection components) { components.forEach(HasSomeMethod::someMethod); // <-- crashes // components.forEach(comp -> comp.someMethod()); <-- works fine } } } 

Encontré una solución para esto fue intercambiar el orden de los generics. Por ejemplo, usa la class A donde necesitas acceder a un método B , o usa la class A si necesitas acceder a un método C Por supuesto, si necesita acceder a métodos de ambas clases, esto no funcionará. Lo encontré útil cuando una de las interfaces era una interfaz de marcador como Serializable .

En cuanto a solucionar 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.