【问题标题】:When is generic return value of function casted after type erasure?类型擦除后何时转换函数的通用返回值?
【发布时间】:2016-01-03 05:05:51
【问题描述】:

这个问题是由this StackOverflow question about unsafe casts: Java Casting method without knowing what to cast to 提出的。在回答我遇到这种行为的问题时,我无法仅根据规范来解释

我在 The Java Tutorials 中找到了以下语句 甲骨文文档:

没有解释“如果需要”的确切含义,并且 我在Java Language Specification 中发现根本没有提及这些演员阵容,所以我开始尝试。

我们来看下面这段代码:

// Java source
public static <T> T identity(T x) {
    return x;
}
public static void main(String args[]) {
    String a = identity("foo");
    System.out.println(a.getClass().getName());
    // Prints 'java.lang.String'

    Object b = identity("foo");
    System.out.println(b.getClass().getName());
    // Prints 'java.lang.String'
}

javac编译,用the Java Decompiler反编译:

// Decompiled code
public static void main(String[] paramArrayOfString)
{
    // The compiler inserted a cast to String to ensure type safety
    String str = (String)identity("foo");
    System.out.println(str.getClass().getName());

    // The compiler omitted the cast, as it is not needed
    // in terms of runtime type safety, but it actually could
    // do an additional check. Is it some kind of optimization
    // to decrease overhead? Where is this behaviour specified?
    Object localObject1 = identity("foo");
    System.out.println(localObject1.getClass().getName());
}

我可以看到在第一种情况下有一个确保类型安全的强制转换, 但在第二种情况下,它被省略了。这是 当然可以,因为我想将返回值存储在 Object 类型化变量,因此根据类型安全性,强制转换不是绝对必要的。然而,它会导致一个有趣的行为与不安全的强制转换:

public class Erasure {
    public static <T> T unsafeIdentity(Object x) {
        return (T) x;
    }

    public static void main(String args[]) {
        // I would expect c to be either an Integer after this
        // call, or a ClassCastException to be thrown when the
        // return value is not Integer
        Object c = Erasure.<Integer>unsafeIdentity("foo");
        System.out.println(c.getClass().getName());
        // but Prints 'java.lang.String'
    }
}

编译和反编译,我没有看到类型转换以确保在运行时返回正确的类型:

// The type of the return value of unsafeIdentity is not checked,
// just as in the second example.
Object localObject2 = unsafeIdentity("foo");
System.out.println(localObject2.getClass().getName());

这意味着如果一个泛型函数应该返回一个给定的对象 类型,不能保证它最终返回该类型。一个 使用上述代码的应用程序将在它尝试的第一个点失败 将返回值转换为Integer,如果它这样做的话,所以我觉得 它打破了fail-fast principle

编译器在插入此强制转换期间的确切规则是什么 确保类型安全的编译以及这些规则在哪里指定?

编辑:

我看到编译器不会深入研究代码并试图证明通用代码确实返回了它应该返回的内容,但它可以插入一个断言,或者至少是一个类型转换(它在特定情况下已经这样做了,如第一个示例所示)以确保返回类型正确,因此后者将抛出 ClassCastException:

// It could compile to this, throwing ClassCastException:
Object localObject2 = (Integer)unsafeIdentity("foo");

【问题讨论】:

  • 我不是这方面的专家,但我认为编译器在这种情况下无法进行任何检查,因为(1)当它看到return (T) x; 行时,它没有办法静态知道x不能转换为T; (2) 当您实际调用unsafeIdentity 时,编译器无法知道这将失败因为它不会深入研究方法的代码并查找将失败的语句。基本上,我认为这意味着方法中对(T) 的强制转换是没有用的。
  • 感谢@ajb,当然转换为 (T) 是没用的,它真的是一个极简的例子。但它可以很容易地将外部函数编译为Object o = (Integer)unsafeIdentity("foo");,这会抛出ClassCastException 还是我遗漏了什么?
  • 我认为编译器不会/必须插入断言,除非您对其进行编码,那么为什么要在这里这样做呢?但问题的其余部分很有趣,+1。
  • 好的,我明白了——该方法被声明为返回一个T,所以我可以看到编译器如何能够在不阅读方法代码的情况下添加此检查。但这会在绝大多数情况下增加不必要的开销,包括许多 Collections 类,例如,get() 方法返回泛型类型。这可能是一个不可接受的权衡。
  • 我不完全理解这个问题。为什么要插入演员表?它在编译 unsafeIdentity 方法时明确警告未经检查的强制转换,从那以后,无论如何都不能保证类型。但是,我认为hg.openjdk.java.net/jdk8/jdk8/langtools/file/756ae3791c45/src/… 在这里可能是相关的,因为它清楚地表明它根本不会在不需要时插入演员表(事实上,这甚至可能是您问题的答案 - 但我不是当然)

标签: java generics type-erasure


【解决方案1】:

如果你在规范中找不到它,那意味着它没有被指定,由编译器实现决定在哪里插入强制转换,只要擦除的代码符合非类型安全规则- 通用代码。

在这种情况下,编译器擦除的代码如下所示:

public static Object identity(Object x) {
    return x;
}
public static void main(String args[]) {
    String a = (String)identity("foo");
    System.out.println(a.getClass().getName());

    Object b = identity("foo");
    System.out.println(b.getClass().getName());
}

在第一种情况下,已擦除代码中的强制转换是必需的,因为如果您删除它,已擦除代码将无法编译。这是因为 Java 保证在运行时保存在可具体化类型的引用变量中的必须是 instanceOf 可具体化类型,因此这里需要运行时检查。

在第二种情况下,已擦除的代码无需强制转换即可编译。是的,如果您添加了演员表,它也会编译。所以编译器可以决定任何一种方式。在这种情况下,编译器决定不插入强制转换。这是一个完全有效的选择。您不应该依赖编译器来决定任何一种方式。

【讨论】:

  • 非常感谢,这清楚地回答了我的问题。我希望有人能在规范中找到我无法找到的相关内容,但看起来唯一提到这一点的是关于类型擦除的 java 教程文章,在 JLS 15.5 中提到(由@HopefullyHelpful 发现)和 OpenJDK 源(由@Marco13 发现)
【解决方案2】:

版本 1 更可取,因为它在 compiletime 失败。

Typesafe 版本 1 非遗留代码:

class Erasure {
public static <T> T unsafeIdentity(T x) {
    //no cast necessary, type checked in the parameters at compile time
    return x;
}

public static void main(String args[]) {
    // This will fail at compile time and you should use Integer c = ... in real code
    Object c = Erasure.<Integer>unsafeIdentity("foo");
    System.out.println(c.getClass().getName());
   }
}

Typesafe 版本 2 遗留代码(A run-time type error [...] In an automatically generated cast introduced to ensure the validity of an operation on a non-reifiable typereference type casting):

class Erasure {
public static <T> T unsafeIdentity(Object x) {
    return (T) x;
    //Compiled version: return (Object) x; 
    //optimised version: return x;
}

public static void main(String args[]) {
    // This will fail on return, as the returned Object is type Object and Subtype Integer is expected, this results in an automatic cast and a ClassCastException:
    Integer c = Erasure.<Integer>unsafeIdentity("foo");
    //Compiled version: Integer c = (Integer)Erasure.unsafeIdentity("foo");
    System.out.println(c.getClass().getName());
   }
}

TypeSafe 版本 3 遗留代码,每次都知道超类型的方法 (JLS The erasure of a type variable (§4.4) is the erasure of its leftmost bound.):

class Erasure {
public static <T extends Integer> T unsafeIdentity(Object x) {
    // This will fail due to Type erasure and incompatible types:
    return (T) x;
    // Compiled version: return (Integer) x;
}

public static void main(String args[]) {
    //You should use Integer c = ...
    Object c = Erasure.<Integer>unsafeIdentity("foo");
    System.out.println(c.getClass().getName());
   }
}

Object 仅用于说明 Object 在版本 1 和 3 中是一个有效的赋值目标,但您应该尽可能使用真实类型或泛型类型。

如果您使用其他版本的 java,您应该查看规范的特定页面,我预计不会有任何变化。

【讨论】:

  • 你写的都是真的,但还是没有回答问题
  • “编译器在编译期间插入此强制转换以确保类型安全的确切规则是什么?这些规则在哪里指定?”我希望看到对相应 JLS 段落的引用(如果有),或者从多个(有时是边缘情况)场景的反编译中扣除一些有根据的猜测
  • 我添加了引用,我只找到了自动引入的间接引用,但是类型擦除后版本2中右手语句的类型是Object,由于类型擦除。这在两个泛型教程中都有说明。
【解决方案3】:

我不能很好地解释它,但是评论不能像我想要的那样添加代码,所以我添加了这个答案。只是希望这个答案可以帮助您理解。评论不能像我想要的那样添加代码。

在您的代码中:

public class Erasure {
    public static <T> T unsafeIdentity(Object x) {
        return (T) x;
    }

    public static void main(String args[]) {
        // I would expect it to fail:
        Object c = Erasure.<Integer>unsafeIdentity("foo");
        System.out.println(c.getClass().getName());
        // but Prints 'java.lang.String'
    }
}

它会在编译后擦除泛型。在编译时, Erasure.unsafeIdentity 没有错误。 jvm 擦除泛型取决于您提供的泛型参数(整数)。之后的功能是这样的?:

public static Integer unsafeIdentity(Object x) {
    return x;
}

其实协变返回会加上Bridge Methods

public static Object unsafeIdentity(Object x) {
    return x;
}

如果函数和上一个一样,你认为你的main方法中的代码会编译失败吗?没有错误。Generics Erasure不会在这个函数中添加强制转换,返回参数不是java函数的缩进。

我的解释有点牵强,希望能帮助你理解。

编辑:

在谷歌关于该主题之后,我猜你的问题是使用桥接方法的协变返回类型。 BridgeMethods

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-04-10
    • 2020-04-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多