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

sdnqo3pr  于 2021-06-30  发布在  Java
关注(0)|答案(2)|浏览(271)

为什么没有编译错误呢 addListener 方法是用参数调用的,参数是带有接口的方法引用 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
pbgvytdp

pbgvytdp1#

这在某种程度上是有道理的,尽管有人可以肯定地说这是不可取的。
问题是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可能会)。不能传递类型为的表达式 Eventconsume a的召唤 Consumer<T> 式中,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 或者某个接口,它可以编译,但是如果它是某个类(final或not),它就不会编译。这大概是因为编译器在想:好吧,这可能会在以后解决,不会发生堆损坏,因为如果不传入t,就无法安全地获取它,然后编译器错误就会接踵而至,除非您有类似的情况 Weird ,上图。
这就引出了一个显而易见的后续问题:
您确实得到了一个运行时异常,它似乎基于类型问题。你在你的问题中说它“可预测”崩溃,但我觉得这不是特别可预测的。你的 addListener 代码什么都不做,通常用泛型擦除就可以了。一些联系过程失败了。
所以,在某些规范中还是有一个bug,大概值得在bugs.openjdk中归档。

xxe27gdn

xxe27gdn2#

这段代码被编译器接受是正确的,因为它与泛型类型系统有关。当界面 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 . 因为我们可以为类型约束提供显式的解决方案,所以类型推断在假设约束可以满足时是正确的。
一个版本工作而另一个在运行时失败的原因与代码生成方式的细微差异有关。当我们查看字节码时,我们将看到在这两种情况下,都会生成一个合成的helper方法,而不是传递 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 无法调用 accept 任何方法 null ,因为它不知道 T 是。
问题代码的有趣之处在于,helper方法声明的参数类型与 listener2 方法,这使得整个helper方法毫无意义。方法必须接受另一个边界( Event )或者只是 Object ,作为第二种情况,使其发挥作用。这似乎是编译器中的一个bug。

相关问题