我有一些带有方法引用的代码,它可以很好地编译并在运行时失败。
例外是:
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 ImageController<E extends BasicEntity & HasImagesEntity> {
void doTheThing(E entity) {
Set<String> filenames = entity.getImages().keySet().stream()
.map(entity::filename)
.collect(Collectors.toSet());
}
}
尝试解析entity::filename
时会抛出异常。 filename()
中声明了HasImagesEntity
。据我所知,我得到了异常,因为E的擦除是BasicEntity
而JVM没有(不能?)考虑E上的其他边界。
当我将方法引用重写为一个普通的lambda时,一切都很好。对我来说,一个构造按预期工作并且它的语义等价物爆炸似乎真的很可疑。
这可能是在规范中吗?我正在努力寻找一种不会在编译器或运行时出现问题的方法,并且没有提出任何建议。
答案 0 :(得分:20)
这是一个简化的示例,它可以重现问题并仅使用核心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
,它没有关于泛型类型的信息。因此,它唯一看到的是原始类型不匹配。
与许多通用构造一样,在字节代码级别上需要一个类型转换,它不会出现在源代码中。由于LambdaMetafactory
明确需要直接方法句柄,因此封装此类型类型转换的方法引用不能作为MethodHandle
传递给工厂。
有两种方法可以解决它。
如果接收者类型是LambdaMetafactory
,则首先解决方案是将MethodHandle
更改为信任interface
并在生成的lambda类中插入所需的类型转换而不是拒绝它。毕竟,它对于参数和返回类型已经类似。
或者,编译器将负责创建一个封装类型转换和方法调用的合成辅助方法,就像编写了一个lambda表达式一样。这不是一个独特的情况。如果您使用方法引用 varargs 方法或数组创建,例如, String[]::new
,它们不能表示为直接方法句柄,最终会出现在合成辅助方法中。
在任何一种情况下,我们都可以将当前行为视为错误。但显然,编译器和JRE开发人员必须就应该在哪个方面处理错误之前达成一致意见。
答案 1 :(得分:14)
我刚刚在JDK9和JDK8u45中解决了这个问题。见this bug。这种变化需要一段时间才能渗透到推广的构建中。 Dan只是向我指出了这个StackOverflow问题,所以我添加了这个注释。当你发现错误时,请提交它们。
我通过让编译器创建一个桥来解决这个问题,这是许多复杂方法引用的方法。我们也正在研究规范的含义。
答案 2 :(得分:11)
此错误并未完全修复。我刚刚在1.8.0_72中遇到LambdaConversionException
,发现Oracle的错误跟踪系统中存在开放的错误报告: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<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
}
}
}
答案 3 :(得分:1)
我找到了解决这个问题的方法是交换泛型的顺序。例如,使用class A<T extends B & C>
您需要访问B
方法的位置,或者如果您需要访问class A<T extends C & B>
方法,请使用C
。当然,如果您需要访问这两个类的方法,这将无法正常工作。当其中一个接口是Serializable
等标记接口时,我发现这很有用。
至于在JDK中解决这个问题,我能找到的唯一信息是openjdk的bug跟踪器上的一些错误,这些错误在版本9中被标记为已解决,这是非常无益的。