为什么泛型方法接受接口的方法引用不能满足泛型的要求



为什么没有编译错误,addListener方法是用argument调用的,argument是一个具有接口NotAnEvent的方法引用,与Event类没有任何共同之处?

public class TestClass {
public static void main(String[] args) {
addListener(TestClass::listener1);
addListener(TestClass::listener2);
}
public static <T extends Event> void addListener(Consumer<T> listener) {
}
public static void listener1(ActualEvent event) {
}
public static void listener2(NotAnEvent event) {
}
public static class Event {
}
public static class ActualEvent extends Event {
}
public interface NotAnEvent {
}
}

上面的代码编译成功,至少使用Intellij Idea 2020.3 Ultimate和JDK 8(以及OpenJDK 11)编译成功,但可以预见,它在发布时会崩溃:

Exception in thread "main" java.lang.BootstrapMethodError: call site initialization exception
at java.lang.invoke.CallSite.makeSite(CallSite.java:341)
at java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:307)
at java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:297)
at ru.timeconqueror.TestClass.main(TestClass.java:8)
Caused by: java.lang.invoke.LambdaConversionException: Type mismatch for lambda argument 0: class ru.timeconqueror.TestClass$Event is not convertible to interface ru.timeconqueror.TestClass$NotAnEvent
at java.lang.invoke.AbstractValidatingLambdaMetafactory.validateMetafactoryArgs(AbstractValidatingLambdaMetafactory.java:267)
at java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:303)
at java.lang.invoke.CallSite.makeSite(CallSite.java:302)
... 3 more

编译器接受此代码是正确的,因为它对于泛型类型系统来说是合理的。虽然接口NotAnEvent不是Event的子类型,但可能存在一个扩展Event并实现NotAnEvent的类型,并且将该类型的使用者传递给方法addListener是有效的。

另请参阅通用返回类型上限-接口与类-令人惊讶的有效代码

我们甚至可以将您的示例修复为在运行时工作:

import java.util.function.Consumer;
public class TestClass {
public static <X extends Event&NotAnEvent> void main(String[] args) {
addListener(TestClass::listener1);
TestClass.<X>addListener(TestClass::listener2);
}
public static <T extends Event> void addListener(Consumer<T> listener) {}
public static void listener1(ActualEvent event) {}
public static void listener2(NotAnEvent event) {}
public static class Event {}
public static class ActualEvent extends Event {}
public interface NotAnEvent {}
}

这个固定版本使用一个类型变量为假设的类型(它仍然不是一个实际的类)分配一个名称,所以我们可以在调用addListener时引用它。由于我们可以为类型约束提供一个显式的解决方案,因此类型推理假设约束可以满足是正确的。

一个版本在运行时工作而另一个版本失败的原因与代码生成方式的细微差异有关。当我们查看字节码时,我们会发现在这两种情况下,都生成了一个合成助手方法,而不是将listener2的引用直接传递给LambdaMetafactory

问题代码:

private static void lambda$main$0(TestClass$NotAnEvent);
Code:
0: aload_0
1: invokestatic  #73                 // Method listener2:(LTestClass$NotAnEvent;)V
4: return

工作版本:

private static void lambda$main$0(java.lang.Object);
Code:
0: aload_0
1: checkcast     #73                 // class TestClass$NotAnEvent
4: invokestatic  #75                 // Method listener2:(LTestClass$NotAnEvent;)V
7: return

在类型擦除发生后,具有多个边界的类型通常会将一个绑定视为声明类型,并将类型强制转换为另一个绑定。对于一个正确的泛型程序,这些强制转换永远不会失败。在您的情况下,方法addListener不能用除null之外的任何东西调用accept方法,因为它不知道T是什么

问题代码中有趣的一点是,helper方法声明的参数类型与listener2方法的参数类型相同,这使得整个helper方法毫无意义。作为第二种情况,该方法必须采用另一个绑定(Event)或仅采用Object才能使其工作。这似乎是编译器中的一个错误。

这在某种程度上是有道理的,尽管人们可以肯定地认为这是不可取的。

问题在于PECS规则(生产者扩展,消费者超级)。想象一下,我们把这个翻转过来:

public class TestClass {
public static void main(String[] args) {
addListener(TestClass::listener2);
}
public static <T extends Event> void addListener(Supplier<T> listener) {}
public static NotAnEvent listener2() {return null;}
public static class Event {}
public static class ActualEvent extends Event {}
public interface NotAnEvent {}
}

这不会编译。这有点奇怪;除了这次我们有一个供应商而不是一个消费者之外,这是100%相同的。

然而,这有点令人费解。我们可以简单地使用那个供应商:Event x = supplier.get();-并且我们得到了一个没有强制转换的classcastexception,编译了这个代码。

但是,您的消费者实际上不能在此处使用。除了null,它通常工作得很好,并且不会因为键入错误而出现运行时异常(当然,NPE可能)。不能将类型为Event的表达式传递给Consumer<T>consume调用,其中t定义为T extends Event。毕竟,如果你有一个Consumer<ChildEvent>,并且表达式解析为class SomeEvent extends Event的一个实例——它显然不是ChildEvent呢?

因此,如果没有为您准备好一个T all,您就无法对这个消费者做任何有用的事情,而java不知何故解决了这个问题。

有两种方法可以"尝试修复此问题",但都会导致编译器错误(注意:我只使用ecj进行了测试):

public class TestClass {
public static void main(String[] args) {
addListener(TestClass::listener2, new Event());
addListener(TestClass::listener2, new NotAnEvent() {});
}
public static <T extends Event> void addListener(Consumer<T> listener, T elem) {}
public static void listener2(Consumer<NotAnEvent> c) {}
public static class Event {}
public static class ActualEvent extends Event {}
public interface NotAnEvent {}
}

但是,这里的两个addListener调用都是编译器错误。我们可以做到这一点,但这有点奇怪:

public class Weird extends Event implements NotAnEvent {}
...
addListener(TestClass::listener2, new Weird());

现在它编译并工作了——至关重要的是,没有发生运行时异常,因为可以Weird的实例传递给NotAnEvent的使用者,并且它工作得很好。

这在一定程度上解释了一些行为:NotAnEvent必须是一个接口:如果listener2的参数类型是Object或某个接口,它会编译,但如果它是某个类(最终类或非最终类),它就不会编译。这可能是因为编译器在想:好吧,这个可能会在稍后解决,并且不会发生堆损坏,因为如果不传入T,就无法安全地获取它,然后就会出现编译错误,除非您有类似Weird的东西。

这就引出了一个显而易见的后续问题:

您确实得到了一个运行时异常,该异常似乎是基于类型问题。你在问题中说它"可预测"地崩溃,但我觉得这不是特别可预测的。您的addListener代码什么都不做,通常使用泛型擦除,这很好。某些链接过程失败。

因此,仍然,这是某个规范中的某个错误,可能值得在bugs.openjdk.上提交

相关内容

  • 没有找到相关文章

最新更新