LambdaConversionException con generici: bug JVM?

Ho del codice con un riferimento al metodo che compila bene e non riesce in fase di runtime.

L’eccezione è così:

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 class è così:

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

Viene generata l’eccezione cercando di risolvere entity :: filename. nomefile () è dichiarato su HasImagesEntity. Vicino come posso dire, ottengo l’eccezione perché la cancellazione di E è BasicEntity e la JVM non (non può?) Considera altri limiti su E.

Quando riscrivo il metodo di riferimento come un lambda banale, tutto va bene. Mi sembra davvero strano che un costrutto funzioni come previsto e il suo equivalente semantico esploda. Questo potrebbe essere nelle specifiche? Sto provando molto a trovare un modo per non rappresentare un problema nel compilatore o nel runtime e non ho trovato nulla.

Ecco un esempio semplificato che riproduce il problema e utilizza solo classi core Java:

 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); } 

La tua ipotesi è corretta, l’implementazione specifica di JRE riceve il metodo di destinazione come MethodHandle che non ha informazioni sui tipi generici. Di conseguenza, l’unica cosa che vede è che i tipi non corrispondenti sono errati.

Come con molti costrutti generici, è richiesto un cast di tipo a livello di codice byte che non appare nel codice sorgente. Poiché LambdaMetafactory richiede esplicitamente un handle di metodo diretto , un riferimento al metodo che incapsula un cast di questo tipo non può essere passato come MethodHandle alla factory.

Ci sono due modi possibili per affrontarlo.

La prima soluzione sarebbe quella di cambiare LambdaMetafactory possa fidare di MethodHandle se il tipo di ricevitore è interface e inserire il tipo richiesto da solo nella class lambda generata invece di rifiutarla. Dopotutto, è già simile per i parametri e i tipi di ritorno.

In alternativa, il compilatore avrebbe il compito di creare un metodo di supporto sintetico che incapsulasse il tipo cast e la chiamata al metodo, proprio come se avessi scritto un’espressione lambda. Questa non è una situazione unica. Se si utilizza un riferimento al metodo su un metodo varargs o su una creazione di array come, ad esempio, String[]::new , non possono essere espressi come handle del metodo diretto e finiscono nei metodi di supporto sintetici.

In entrambi i casi, possiamo considerare il comportamento corrente un bug. Ma ovviamente, gli sviluppatori di compilatori e JRE devono concordare in che modo deve essere gestito prima di poter dire da quale parte risiede il bug.

Ho appena risolto questo problema in JDK9 e JDK8u45. Vedi questo bug . Il cambiamento richiederà un po ‘di tempo per passare alle build promosse. Dan mi ha appena indicato questa domanda StackOverflow, quindi aggiungo questa nota. Quando trovi bug, per favore inviali.

Ho affrontato questo problema facendo in modo che il compilatore crei un bridge, come lo è l’approccio per molti casi di riferimenti di metodi complessi. Stiamo anche esaminando le implicazioni delle specifiche.

Questo bug non è completamente risolto. Ho appena eseguito una LambdaConversionException in 1.8.0_72 e ho visto che ci sono segnalazioni di bug aperte nel sistema di tracciamento dei bug di Oracle: link1 , link2 .

(Modifica: i bug collegati sono segnalati per essere chiusi in JDK 9 b93)

Come soluzione alternativa, evito le maniglie del metodo. Quindi invece di

 .map(entity::filename) 

lo voglio

 .map(entity -> entity.filename()) 

Ecco il codice per riprodurre il problema su 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 } } } 

Ho trovato una soluzione per questo è stato lo scambio di ordine dei generici. Ad esempio, utilizzare la class A dove è necessario accedere a un metodo B , oppure utilizzare la class A se è necessario accedere a un metodo C Naturalmente, se hai bisogno di accedere ai metodi di entrambe le classi, questo non funzionerà. L’ho trovato utile quando una delle interfacce era un’interfaccia marcatore come Serializable .

Per quanto riguarda la correzione di questo nel JDK, le uniche informazioni che ho trovato erano alcuni bug sul bug tracker di openjdk che sono contrassegnati risolti nella versione 9 che è piuttosto inutile.