带generics的LambdaConversionException:JVM错误?

我有一些带有方法引用的代码,它可以很好地编译并在运行时失败。

例外是这样的:

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) 

这个class是这样的:

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

尝试解析entity :: filename会抛出exception。 filename()在HasImagesEntity上声明。 接近我可以说,我得到了exception,因为E的擦除是BasicEntity而JVM没有(不能?)考虑E上的其他边界。

当我将方法引用重写为一个普通的lambda时,一切都很好。 对我来说,一个构造按预期工作并且它的语义等价物爆炸似乎真的很可疑。 这可能是在规范中吗? 我正在努力寻找一种不会在编译器或运行时出现问题的方法,并且没有提出任何建议。

这是一个简化的示例,它可以重现问题并仅使用核心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); } 

您的假设是正确的,JRE特定的实现接收目标方法作为MethodHandle ,它没有关于generics类型的信息。 因此,它唯一看到的是原始类型不匹配。

与许多通用构造一样,在字节代码级别上需要一个类型转换,它不会出现在源代码中。 由于LambdaMetafactory明确需要直接方法句柄,因此封装此类型类型转换的方法引用不能作为MethodHandle传递给工厂。

有两种可能的方法来处理它。

如果接收器类型是interface并且在生成的lambda类中插入所需的类型转换而不是拒绝它,则第一种解决方案是将LambdaMetafactory更改为信任MethodHandle 。 毕竟,它已经类似于参数和返回类型。

或者,编译器将负责创建封装类型转换和方法调用的合成辅助方法,就像您编写了lambda表达式一样。 这不是一个独特的情况。 如果使用方法引用varargs方法或数组创建(例如String[]::new ,则它们不能表示为直接方法句柄,最终会出现在合成辅助方法中。

在任何一种情况下,我们都可以将当前行为视为错误。 但显然,编译器和JRE开发人员必须就应该在哪个方面处理错误之前达成一致意见。

我刚刚在JDK9和JDK8u45中解决了这个问题。 看到这个bug 。 这种变化需要一段时间才能渗透到推广的构建中。 Dan只是向我指出了这个StackOverflow问题,所以我正在添加这个注释。 当你发现bug时,请提交它们。

我通过让编译器创建一个桥来解决这个问题,这是许多复杂方法引用的方法。 我们也在研究规范含义。

这个bug并没有完全修复。 我刚刚在LambdaConversionException中遇到了LambdaConversionException ,发现Oracle的bug跟踪系统中有开放的bug报告: link1 , link2 。

(编辑:报告链接的错误在JDK 9 b93中关闭)

作为一个简单的解决方法,我避免方法句柄。 而不是

 .map(entity::filename) 

我做

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

这是在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 } } } 

我找到了一个解决方法,就是交换generics的顺序。 例如,使用class A ,您需要访问B方法,或者如果需要访问C方法,则使用class A 。 当然,如果您需要访问两个类中的方法,这将无效。 当其中一个接口是Serializable等标记接口时,我发现这很有用。

至于在JDK中解决这个问题,我可以找到的唯一信息是openjdk的bug跟踪器上的一些错误,这些错误在版本9中被标记为已解决,这是相当无益的。