Java Casting: Java 11 throws LambdaConversionException while 1.8 does not

TL;DR The Eclipse compiler generates a method signature for the lambda instance that is invalid according to the specification. Due to additional type checking code added in JDK 9 to better enforce the specification, the incorrect signature is now causing an exception when running on Java 11.


Verified with Eclipse 2019-03 as well with this code:

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

Even when using null as the receiver, the code fails when bootstrapping with the same error.

Using javap -v Main we can see where the problem lies. I'm seeing this in the BoostrapMethods table:

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

Note that the last argument (constant #54) is (Ljava/lang/Object;)V, while javac generates (Lmain/HasValue$ValueChangeEvent;)V. i.e. the method signature that Eclipse wants to use for the lambda is different from what javac wants to use.

If the wanted method signature is the erasure of the target method (which seems to be the case), then the correct method signature is indeed (Lmain/HasValue$ValueChangeEvent;)V since that is the erasure of the target method, which is:

void valueChanged(E event);

Where E is E extends HasValue.ValueChangeEvent<?>, so that would be erased to HasValue.ValueChangeEvent.

The problem seems to be with ECJ, and seems to have been brought to the surface by JDK-8173587 (revision) (Unfortunately this seems to be a private ticket.) which adds extra type checks to verify that the SAM method type is actually compatible with the instantiate method type. According to the documentation of LambdaMetafactory::metafactory the instantiated method type must be the same, or a specialization of the SAM method type:

instantiatedMethodType - The signature and return type that should be enforced dynamically at invocation time. This may be the same as samMethodType, or may be a specialization of it.

which the method type generated by ECJ is evidently not, so this ends up throwing an exception. (though, to be fair, I don't see defined anywhere what constitutes a "specialization" in this case). I've reported this on the Eclipse bugzilla here: https://bugs.eclipse.org/bugs/show_bug.cgi?id=546161

I'm guessing this change was made somewhere in JDK 9, since source code was already modular at that point, and the date of the revision is fairly early (February 2017).

Since javac generates the correct method signature, you could switch to that for the time being as a workaround.