Casting Java: Java 11 genera LambdaConversionException mentre 1.8 no



eclipse java-8 (1)

il codice seguente funziona perfettamente in una VM Java 1.8 ma produce una LambdaConversionException quando eseguito in una VM Java 11. Dov'è la differenza e perché si comporta così?

Codice:

public void addSomeListener(Component comp){
    if(comp instanceof HasValue) {
        ((HasValue<?,?>) comp).addValueChangeListener(evt -> {
            //do sth with evt
        });
    }
}

HasValue Javadoc

Eccezione (solo V11):

Caused by: java.lang.invoke.LambdaConversionException: Type mismatch
for instantiated parameter 0: class java.lang.Object is not a subtype
of interface com.vaadin.flow.component.HasValue$ValueChangeEvent
    at java.base/java.lang.invoke.AbstractValidatingLambdaMetafactory.checkDescriptor(AbstractValidatingLambdaMetafactory.java:308)
    at java.base/java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:294)
    at java.base/java.lang.invoke.LambdaMetafactory.altMetafactory(LambdaMetafactory.java:503)
    at java.base/java.lang.invoke.BootstrapMethodInvoker.invoke(BootstrapMethodInvoker.java:138)
    ... 73 more

Soluzione:

ValueChangeListener<ValueChangeEvent<?>> listener = evt -> {
    // do sth with evt
};
((HasValue<?,?>) comp).addValueChangeListener(listener);

Sistema:
Sistema operativo: Windows 10
IDE: Eclipse 2018-12 (4.10.0)
Java (compilazione): ecj
Java (Webserver): JDK 11.0.2
Server web: Wildfly 15


TL; DR Il compilatore Eclipse genera una firma del metodo per l'istanza lambda non valida secondo la specifica. A causa del codice di controllo del tipo aggiuntivo aggiunto in JDK 9 per applicare meglio le specifiche, la firma errata sta causando un'eccezione durante l'esecuzione su Java 11.

Verificato anche con Eclipse 2019-03 con questo codice:

public class Main {    
    public static void main(String[] args) {
        getHasValue().addValueChangeListener(evt -> {});
    }

    public static HasValue<?, ?> getHasValue() {
        return null;
    }    
}

interface HasValue<E extends HasValue.ValueChangeEvent<V>,V> {    
    public static interface ValueChangeEvent<V> {}    
    public static interface ValueChangeListener<E extends HasValue.ValueChangeEvent<?>> {
        void valueChanged(E event);
    }    
    void addValueChangeListener(HasValue.ValueChangeListener<? super E> listener);
}

Anche quando si utilizza null come ricevitore, il codice non riesce quando si avvia il bootstrap con lo stesso errore.

Usando javap -v Main possiamo vedere dove si trova il problema. Lo vedo nella tabella BoostrapMethods:

BootstrapMethods:
  0: #48 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #50 (Lmain/HasValue$ValueChangeEvent;)V
      #53 REF_invokeStatic main/Main.lambda$0:(Ljava/lang/Object;)V
      #54 (Ljava/lang/Object;)V

Si noti che l'ultimo argomento (costante # 54) è (Ljava/lang/Object;)V , mentre javac genera (Lmain/HasValue$ValueChangeEvent;)V cioè la firma del metodo che Eclipse vuole usare per la lambda è diversa da quella che javac vuole usare.

Se la firma del metodo desiderato è la cancellazione del metodo target (che sembra essere il caso), allora la firma del metodo corretto è effettivamente (Lmain/HasValue$ValueChangeEvent;)V poiché quella è la cancellazione del metodo target, che è:

void valueChanged(E event);

Dove E è E extends HasValue.ValueChangeEvent<?> , In modo che venga cancellato in HasValue.ValueChangeEvent .

Il problema sembra riguardare la Corte di giustizia europea e sembra essere stato portato in superficie da JDK-8173587 ( revision ) (purtroppo questo sembra essere un biglietto privato.) Che aggiunge ulteriori controlli del tipo per verificare che il tipo di metodo SAM sia effettivamente compatibile con il tipo di metodo di istanza. Secondo la documentazione di LambdaMetafactory::metafactory il tipo di metodo istanziato deve essere lo stesso o una specializzazione del tipo di metodo SAM:

instantiatedMethodType: il tipo di firma e di ritorno che deve essere applicato in modo dinamico al momento dell'invocazione. Potrebbe essere uguale a samMethodType o essere una sua specializzazione.

che il tipo di metodo generato da ECJ non è evidentemente, quindi finisce per generare un'eccezione. (sebbene, per essere onesti, non vedo definito da nessuna parte ciò che costituisce una "specializzazione" in questo caso). L'ho segnalato sul bugzilla di Eclipse qui: https://bugs.eclipse.org/bugs/show_bug.cgi?id=546161

Immagino che questa modifica sia stata apportata da qualche parte in JDK 9, poiché il codice sorgente era già modulare a quel punto e la data della revisione è abbastanza presto (febbraio 2017).

Poiché javac genera la firma del metodo corretta, è possibile passare a quello per il momento come soluzione alternativa.





vaadin-flow