繁体   English   中英

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

[英]Why does generic method accept method reference with interface which doesn't satisfy generic's requirements

为什么没有编译错误, addListener方法是用参数调用的,这是一个带有接口NotAnEvent的方法引用,它与Event class 没有任何共同之处?

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是有效的。

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

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

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,编译了这段代码。

但是,您的 Consumer实际上不能在这里使用 除了null ,它可以正常工作,并且不会因输入错误而发生运行时异常(当然,NPE 可能)。 您不能将Event类型的表达式传递给Consumer<T>consume调用,其中 T 定义为T extends Event 毕竟,如果您有一个Consumer<ChildEvent>并且表达式解析为class SomeEvent extends Event的实例 - 这显然不是ChildEvent怎么办?

因此,如果没有为您准备好 go 的 T ,您将无法对这个消费者做任何有用的事情,并且 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或某个接口,它会编译,但如果它是一些 class(最终或不是),它不会。 这大概是因为编译器正在考虑:好吧,这可能会在以后解决,并且不会发生堆损坏,因为没有办法安全地获取 T 而不传递它,然后会出现编译器错误,除非你有类似Weird的东西, 以上。

这让我们想到了明显的后续问题:

确实会得到一个似乎基于键入问题的运行时异常。 你在你的问题中说它“可预测地”崩溃,但我觉得这不是特别可预测的。 您的addListener代码不执行任何操作,通常使用 generics 擦除,这很好。 某些链接过程失败。

所以,仍然是某个规范中的一个错误,大概值得在 bugs.openjdk 提交。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM