【问题标题】:How java erasure decided which cast to make to return type?java擦除如何决定返回类型的转换?
【发布时间】:2019-10-21 22:44:12
【问题描述】:

我花了很多时间学习 Java 的通用特性。我试图阅读以下链接中的解释:https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html,我不明白,也试图通过论坛的以下链接来理解这个问题:Java Type Erasure: Rules of cast insertion?,但事情还远未弄清楚我,我还添加了评论,但我怀疑是否有人会引用评论,因为帖子很旧。

我试图了解编译器在进行擦除时如何知道为方法返回类型进行的转换。在我正在学习的书中(Deital - Java How to Program),我认为解释中的某些内容丢失了(可能是错字)。那里写着:“在每种情况下,返回值的类型转换都是从特定方法调用中的方法参数的类型推断出来的,因为根据方法声明, 返回类型和参数类型匹配。 据我理解这句话,它说演员表是根据调用方法时发送的参数。但是如果方法没有得到任何参数。例如,如果方法原型是:

public static <T extends Animal> T funcA ()

现在有 3 种不同的方法调用该函数。一个想要得到一个返回类型的 Dog (Dog d = funcA()),第二个想要得到 Cat (Cat c = funcA()),最后一个想要得到 Animal (Animal A = funcA())。 擦除如何决定制作哪个演员表?

【问题讨论】:

    标签: java generics casting type-erasure


    【解决方案1】:

    这实际上与擦除没有任何关系。从调用站点推断类型:调用者“实际期望的类型”。Animal a = funcA() 等价于Animal a = MyClass.&lt;Animal&gt;funcA()Dog d = funcA() 等价于Dog d = MyClass.&lt;Dog&gt;funcA()

    这是否在 实现 级别上工作确实与擦除有关,并且实际上没有针对 funcA 的有效实现——不会有编译器警告的实现,或者等效地,在运行时不会失败——除了(归结为)return null

    【讨论】:

    • funcA 实际上没有有效的实现” 当然有,因为 erasure 导致代码编译为 @ 987654328@,任何返回Animal(或其子类型)的实现都可以。问题完全是关于擦除,以及编译器如何使其工作,例如编译器如何使用擦除使Dog d = funcA() 编译为(等效于)Dog d = (Dog) funcA()
    • 抱歉,我将“有效”读作“可编译”,现在我意识到这可能不是您的意思。没错,没有任何实现可以使所有 3 个调用都正常运行,因为擦除意味着 funcA() 中的代码不(不能)知道调用者期望哪种类型。我的观点仍然存在,因为代码会编译,因为编译器添加了强制转换,这就是问题所在。
    • 如果你能澄清你的意思,那么我将允许我删除我给你的反对票。
    • @Louis Wasserman,你能解释一下下面的语法吗:'MyClass.funcA()'?
    • MyClass.&lt;Animal&gt;funcA() 明确强制 T 中的 public static &lt;T extends Animal&gt; T funcA()Animal。我假设funcAMyClass 类中的一个方法。
    【解决方案2】:

    手头有 3 个不同的问题:

    • 实际方法的类型擦除

    • 覆盖继承方法时的桥接方法

    • 调用方法时进行强制转换


    你的例子是:

    public static <T extends Animal> T funcA ()
    

    该方法与桥接方法没有任何关系,因为它是静态的。

    类型擦除意味着编译器消除(擦除)泛型类型,因此代码被编译为就像您编写的一样:

    public static Animal funcA ()
    

    因此,当您随后调用该方法时,编译器会添加强制转换:

    Dog d = (Dog) funcA()
    

    如你所见,编译后的代码没有泛型类型,都是编译器实现的“魔法”,假装泛型确实存在。


    现在,如果您真的对桥接方法以及擦除的工作原理感兴趣,那么让我们看看您提供的 link 中的示例。

    在那篇文章中,基类中的方法声明为:

    public class Node<T> {
        public void setData(T data)
    

    Erasure 将其编译为:

    public class Node {
        public void setData(Object data)
    

    在子类中方法声明为:

    public class MyNode extends Node<Integer> {
        public void setData(Integer data)
    

    Erasure 将其编译为:

    public class MyNode extends Node {
        public void setData(Integer data)
    

    现在编译器添加了一个桥接方法,因为 JVM 需要在子类中使用 Object 参数类型的覆盖方法:

    @Override
    public void setData(Object data) {
        setData((Integer) data);
    }
    

    如您所见,编译器再次添加了强制转换。我希望这能回答您关于编译器如何知道要进行哪个强制转换以及何时进行擦除的问题。

    【讨论】:

    • 嗨,一些问题,因为我觉得我无法理解我得到的任何答案(我的错): 1. 在代码中:Dog d = (Dog) funcA (),谁添加了 Dog 演员? 2."现在编译器添加了一个桥接方法,因为JVM需要在子类中覆盖Object参数类型的方法",为什么子类需要覆盖setData(Object Data) 方法?
    • @Eitanos30 1. 再次尝试阅读答案。 Dog d = (Dog) funcA() 之前的那行说“... 转换是由 compiler 添加的” --- 2. MyNode extends Node&lt;Integer&gt;,所以 Node 中的 setData(T data) 方法是 @ 987654337@,这意味着MyNode中的setData(Integer data)方法是一个覆盖。从逻辑上讲,从 Java 的角度来看。但是 JVM 的工作方式有点不同,因此编译需要添加一个桥接方法来“弥合”Java 看待世界的方式和 JVM 实际运行世界的方式之间的差距。
    • 1.我知道编译器制作了 Dog 演员表,但根据什么,他做了演员表?根据分配语句的左侧? 2. 为什么说它覆盖了方法。它不是只继承 Node 类的方法吗?抱歉,但我不明白为什么方法是“加倍”并且不只存在一次?如果很难更好地解释你已经没关系。我发现我在理解这个问题方面存在巨大差距
    • @Eitanos30 1. 根据编译器对T 的类型推断,即Dog。如果您想了解 类型推断 的工作原理,请阅读 Java 语言规范的 Chapter 18. Type Inference
    • @Eitanos30 2. 根据 Java 泛型MyNode.setData(Integer) 方法是对Node.setData(T) 的覆盖,因为T 被声明为Integer (@ 987654346@)。那是Java。 --- 根据JVMMyNode.setData(Integer)方法不是Node.setData(Object)方法的覆盖,因为覆盖方法必须有签名setData(Object),所以需要MyNode.setData(Object)方法“弥合差距”。
    【解决方案3】:

    基于以下语句中的目标类型

    • 狗 d = funcA()

    • Cat c = funcA()

    • 动物 A = funcA()

    编译器正在使用类型推断 (https://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html)。如果您查看“目标类型”部分中的示例,您将看到与您所要求的内容相似的内容。

    【讨论】:

      【解决方案4】:

      通用代码中根本没有强制转换。在评估强制转换表达式时,Java 对象引用是二进制不变的。任何引用类型始终只是object。其他一切都是编译器的保证。所以没有必要在通用代码中进行强制转换。

      事实上,演员表只生成一些类似的代码

      if (!(obj instanceof desiredtype))
          throw new ClassCastException();
      

      然而,Java 允许将 无类型类型 转换为泛型类型。这可能导致函数返回错误的类型。由调用者来处理这种情况。三个调用者知道他们各自的预期类型。因此,他们可以在需要时检查意外结果(即演员表)。

      【讨论】:

      • 我不认为这是正确的。编译器确实在泛型方法的调用点插入强制转换。例如,如果调用者有List&lt;String&gt;,则get(int) 的结果将隐式转换为String,如果结果不是String,则抛出ClassCastException(如果有关类型的警告可能会发生这种情况-安全被忽略)。如果这不是你的意思,请澄清你的答案。
      • 你是对的,它可能被误解了。但重要的是,不能保证调用者站点的演员表。有一些情况,例如使用没有生成此类转换的 Lambda 表达式。对于像泛型返回类型这样的琐碎情况,会执行强制转换,但不会像 OP 假设的那样在泛型代码中执行。
      猜你喜欢
      • 2020-04-17
      • 1970-01-01
      • 2019-04-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多