Java Casting:Java 11 抛出 LambdaConversionException,而 1.8 不抛出

2022-08-31 16:14:20

以下代码在 Java 1.8 VM 中运行良好,但在 Java 11 VM 中执行时会生成一个。区别在哪里,为什么它表现得这样?LambdaConversionException


法典:

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

HasValue Javadoc

异常(仅限 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

解决方法:

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

System:
OS: Windows 10
IDE: Eclipse 2018-12 (4.10.0)
Java (Compile): ecj
Java (Webserver): JDK 11.0.2
Webserver: Wildfly 15


答案 1

TL;DR Eclipse 编译器为 lambda 实例生成一个方法签名,该签名根据规范无效。由于在 JDK 9 中添加了额外的类型检查代码以更好地强制实施规范,因此在 Java 11 上运行时,不正确的签名现在会导致异常。


已通过 Eclipse 2019-03 以及以下代码进行了验证:

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

即使用作接收器,代码在引导时也会失败,并出现相同的错误。null

使用,我们可以看到问题所在。我在BoostrapMethods表中看到了这一点:javap -v Main

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

请注意,最后一个参数(常量 #54)是 ,而生成 。也就是说,Eclipse 想要用于 lambda 的方法签名与要使用的方法签名不同。(Ljava/lang/Object;)Vjavac(Lmain/HasValue$ValueChangeEvent;)Vjavac

如果想要的方法签名是目标方法的擦除(似乎就是这种情况),那么正确的方法签名确实是,因为这是目标方法的擦除,即:(Lmain/HasValue$ValueChangeEvent;)V

void valueChanged(E event);

Where 是 ,因此将被删除到 。EE extends HasValue.ValueChangeEvent<?>HasValue.ValueChangeEvent

问题似乎出在 ECJ 上,并且似乎已经通过 JDK-8173587修订版)浮出水面(不幸的是,这似乎是一个私有票证),它添加了额外的类型检查来验证 SAM 方法类型是否确实与实例化方法类型兼容。根据 LambdaMetafactory::metafactory 的文档,实例化的方法类型必须相同,或者是 SAM 方法类型的专用化:

实例化方法类型 - 应在调用时动态强制实施的签名和返回类型。这可能与 samMethodType 相同,也可能是它的特化。

ECJ生成的方法类型显然不是,所以这最终会引发异常。(不过,公平地说,我没有看到任何地方定义在这种情况下构成“专业化”的内容)。我在这里报告了Eclipse bugzilla:https://bugs.eclipse.org/bugs/show_bug.cgi?id=546161

我猜这个变化是在JDK 9的某个地方进行的,因为源代码在那时已经是模块化的,并且修订的日期相当早(2017年2月)。

由于生成了正确的方法签名,因此您可以暂时切换到该签名作为解决方法。javac


答案 2

推荐