LambdaConversionException与generics: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<E extends BasicEntity & HasImagesEntity> { void doTheThing(E entity) { Set<String> filenames = entity.getImages().keySet().stream() .map(entity::filename) .collect(Collectors.toSet()); } }
引发exception是为了parsingentity :: filename。 filename()在HasImagesEntity上声明。 据我所知,我得到了这个exception,因为E的擦除是BasicEntity,而JVM不能(不能)考虑E上的其他边界。
当我重写方法引用作为一个微不足道的lambda,一切都很好。 对我来说,一个结构按预期工作,其语义相当于爆炸似乎真的很可疑。 这可能是在规范? 我试图很难find一种方法,这不是在编译器或运行时的问题,并没有提出任何事情。
下面是一个简化的例子,它重现了这个问题,只使用了核心的Java类:
public static void main(String[] argv) { System.out.println(dummy("foo")); } static <T extends Serializable&CharSequence> int dummy(T value) { return Optional.ofNullable(value).map(CharSequence::length).orElse(0); }
您的假设是正确的,特定于JRE的实现将目标方法作为MethodHandle
接收,而MethodHandle
没有关于genericstypes的信息。 因此唯一看到的是原始types不匹配。
就像大量的generics结构一样,在字节代码级别上需要进行types转换,而不会出现在源代码中。 由于LambdaMetafactory
明确需要直接方法句柄,因此封装这种types转换的方法引用不能作为MethodHandle
传递给工厂。
有两种可能的方法来处理它。
第一种解决scheme是,如果接收方types是interface
,则将MethodHandle
更改为信任MethodHandle
,并将生成的lambda类中的所需types自身插入到生成的lambda类中,而不是拒绝它。 毕竟,它的参数和返回types已经类似。
或者,编译器负责创build一个封装了types转换和方法调用的合成帮助器方法,就像你写了一个lambdaexpression式一样。 这不是一个独特的情况。 如果您使用方法引用varargs方法或创build数组(如String[]::new
,则不能将其表示为直接方法句柄,而是以合成辅助方法结束。
无论哪种情况,我们都可以考虑当前的行为是一个错误。 但是显然,编译器和JRE开发者必须同意,在我们可以说错误所在的哪一方面之前,应该以哪种方式来处理它。
我刚刚在JDK9和JDK8u45中解决了这个问题。 看到这个错误 。 这一变化需要一段时间才能渗透到促销版本中。 Dan只是指着我这个StackOverflow问题,所以我添加了这个注释。 当你发现错误,请提交它们。
我通过让编译器创build一个桥梁来解决这个问题,就像许多复杂方法引用的情况一样。 我们也在研究规范的含义。
这个bug并不完全是固定的。 我刚刚在LambdaConversionException
中遇到了一个LambdaConversionException
,并且发现在Oracle的bug跟踪系统中有开放的bug报告: link1 , link2 。
(编辑:链接的错误报告将在JDK 9 b93中closures)
作为一个简单的解决方法,我避免了方法句柄。 所以,而不是
.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<ComponentWithSomeMethod>().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<T extends Component & HasSomeMethod> { public void callSomeMethod(Collection<T> components) { components.forEach(HasSomeMethod::someMethod); // <-- crashes // components.forEach(comp -> comp.someMethod()); <-- works fine } } }
我发现一个解决方法是交换generics的顺序。 例如,如果需要访问C
方法,请使用class A<T extends B & C>
,如果需要访问B
方法,或者使用class A<T extends C & B>
。 当然,如果你需要访问这两个类的方法,这是行不通的。 当其中一个接口是像Serializable
这样的标记接口时,我发现这很有用。
至于在JDK中解决这个问题,我能find的唯一信息是openjdk的bug跟踪器上的一些错误,它们在版本9中被标记为已解决,而这是无益的。