【问题标题】:Does type erasure of Java Generics cause full type casting?Java 泛型的类型擦除会导致完全类型转换吗?
【发布时间】:2013-07-19 17:20:54
【问题描述】:

我知道,当 Java 编译器编译泛型类型时,它会执行类型擦除并从代码中删除对泛型的所有引用,我的 ArrayList<Cheesecake> 变成了 ArrayList

我没有明确答案的问题是,缺少类型(因此强制类型转换)是否会导致速度变慢。换句话说,如果我使用的是标准 Java 编译器和 Oracle 的标准 JVM 1.7:

  • 字节码是否包含类型转换?
  • 如果是,是否包含运行时检查以检查其类型是否正确?
  • 转换本身是否需要大量时间?
  • 会创建我自己的CheesecakeList 类,看起来与ArrayList 相同,只是将所有Objects 都转换为Cheesecakes 我会得到什么(再次只是一个假设,我对任何一个都不感兴趣由于更多的类或我的代码中的重复,拥有更大的二进制文件的副作用)。

我对类型擦除导致的任何其他问题不感兴趣,只是一个比我更了解 JVM 的人的简单回答。

我最感兴趣的是这个例子中的成本:

List<Cheesecake> list = new ArrayList<Cheesecake>();
for (int i = 0; i < list.size(); i++)
{
   // I know this gets converted to ((Cheesecake)list.get(i)).at(me)
   list.get(i).eat(me); 
}

与以下相比,在循环内进行转换是否昂贵和/或重要:

CheesecakeList list = new CheesecakeList();
for (int i = 0; i < list.size(); i++)
{
   //where the return of list.get() is a Cheesecake, therefore there is no casting.
   list.get(i).eat(me); 
}

免责声明:这主要是学术好奇心的问题。我真的怀疑类型转换是否存在任何重大的性能问题,如果我在代码中发现性能错误,那么消除对它们的需求甚至不会是我会做的前 5 件事之一。如果您正在阅读本文是因为您确实遇到了性能问题,请帮自己一个忙,并启动一个分析器并找出瓶颈在哪里。如果你真的相信它的类型转换,那么你才应该尝试以某种方式优化它。

【问题讨论】:

    标签: java performance generics


    【解决方案1】:

    简短的故事:是的,有一个类型检查。这是证据-

    给定以下类:

    // Let's define a generic class.
    public class Cell<T> {
      public void set(T t) { this.t = t; }
      public T get() { return t; }
      
      private T t;
    }
    
    public class A {
    
      static Cell<String> cell = new Cell<String>();  // Instantiate it.
      
      public static void main(String[] args) {
        // Now, let's use it.
        cell.set("a");
        String s = cell.get(); 
        System.out.println(s);
      }  
    }
    

    A.main()编译成的字节码(通过javap -c A.class反编译)​​如下:

    public static void main(java.lang.String[]);
    Code:
       0: getstatic     #20                 // Field cell:Lp2/Cell;
       3: ldc           #22                 // String a
       5: invokevirtual #24                 // Method p2/Cell.set:(Ljava/lang/Object;)V
       8: getstatic     #20                 // Field cell:Lp2/Cell;
      11: invokevirtual #30                 // Method p2/Cell.get:()Ljava/lang/Object;
      14: checkcast     #34                 // class java/lang/String
      17: astore_1      
      18: getstatic     #36                 // Field java/lang/System.out:Ljava/io/PrintStream;
      21: aload_1       
      22: invokevirtual #42                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      25: return        
    }
    

    您可以在偏移量14 中,对cell.get() 的结果进行类型检查以验证它确实是一个字符串:

     14: checkcast     #34                 // class java/lang/String
    

    这确实会导致一些运行时损失。但是,由于 JVM 已经优化得非常好,因此这样做的影响可能很小。

    更长的故事:

    您将如何实现这样的CheesecakeList 类?这个类不会定义一个数组来保存元素吗?请注意,对数组的每个赋值都会导致隐藏类型检查。所以你不会得到你想象的那么多(尽管你的程序可能会执行比写操作更多的读操作,所以Cheesecake[] 数组会给你一些东西)。

    底线:不要过早优化。

    最后的评论。人们通常认为类型擦除意味着Cell&lt;String&gt;被编译成Cell&lt;Object&gt;。这不是真的。擦除仅适用于泛型类/方法的定义。不适用于这些类/方法的使用场所。

    换句话说,类Cell&lt;T&gt; 被编译为好像它被写为Cell&lt;Object&gt;。如果T 有一个上限(比如Number),那么它会被编译成Cell&lt;Number&gt;。在代码的其他地方,通常是类型为Cell 类的实例化的变量/参数(例如Cell&lt;String&gt; myCell),不应用擦除。 myCellCell&lt;String&gt; 类型的事实保存在类文件中。这允许编译器对程序进行正确的类型检查。

    【讨论】:

    • “底线:不要过早优化。” 这个。 +1
    • 这是一个纯粹的教育问题。但是你说类型转换存在并且 JVM 处理得非常好,这是我想从这个问题中得到的两个主要内容。
    • 另外值得注意的是,运行时检查是 CPU 的分支预测将能够非常轻松地处理的一项,这将进一步提高性能。
    【解决方案2】:

    类型检查在编译时完成。如果你这样做:

    List<Cheesecake> list = new ArrayList<Cheesecake>();
    

    然后可以在编译时检查泛型类型。这将擦除到:

    List list = new ArrayList();
    

    这与任何其他向上转换没有什么不同(例如Object o = new Integer(5);)。

    【讨论】:

    • 我明白了,我不明白这是多少成本。此外,当我执行 list.get(0).Eat(me) 时,它必须编译为 ((Cheesecake)list.get(0)).Eat(me) 是那个强制转换(可能/很可能在循环中发生)显着
    • 我也不确定成本是多少(我猜向上转换的成本为零)。但它肯定不会比Object o = new Integer(5); 贵。
    • 即使有成本,它也大大超过了不使用泛型时可能出现的运行时异常
    • 但是第二个例子很有趣,因为你是对的,它有一个隐含的向下转换。不知道怎么处理的。
    • 虚拟方法调度可能会花费更多。这一切都进行了高度优化。擦除运行时类型的一个优点是“通用”对象不需要携带太多类型信息。
    【解决方案3】:

    如果您获取ArrayList 的JDK 源代码,并将其克隆到CheesecakeList,并将ArrayList 中出现的所有Object 更改为Cheesecake,则结果(假设您正确操作)将是一个您可以在您的应用程序中使用的类,并且需要少一个 checkcast 操作,而不是使用 ArrayList&lt;Cheesecake&gt;

    也就是说,checkcast 操作非常快,尤其是当您检查的对象属于命名类而不是子类时。它的存在对于轻微扰乱 JITC 优化可能比实际执行成本更重要。

    【讨论】:

      【解决方案4】:

      字节码是否包含类型转换?

      是的。类型擦除在this tutorial中进行了简要描述

      如果是,是否包括运行时检查以检查其类型是否正确?

      没有。只要您没有任何警告,就可以在编译时保证类型安全。 但是有一些桥接方法如上面教程中所述使用

      [编辑:澄清“否”意味着编译器没有因使用泛型而插入附加运行时检查。仍然会在转换期间执行任何运行时检查]

      转换本身是否需要大量时间?

      转换是指铸造吗?如果是,那么即使您没有泛型也是如此

      会制作我自己的CheesecakeList 类,看起来与ArrayList 相同,...

      一般来说,这不是一个简单的问题要回答,当然也不能不考虑其他因素。首先,我们不再谈论泛型。您在问专业课程是否比ArrayList 更快。 假设是的,有一个可以避免强制转换的特殊类应该更快。但真正的问题是你应该担心它吗?

      要回答最后一个问题,您必须具体。每个案例都是不同的。例如,我们在谈论多少个对象?百万?重复通话?额外的时间在程序的表现中是否值得注意?你计时了吗?我们希望我们的数据在未来如何扩展?等等等等

      也不要忘记编译器会不断发展。未来的版本可能会进行优化,自动修复性能问题(也就是说,如果你能忍受它),而如果你坚持使用变得无用的代码库,它可能会一直留在项目中.

      所以我的建议是 a) 确定你在这个特定领域有问题,否则不要试图超越编译器或语言本身 b) 为你的代码计时,为你想要达到的目标设定一些标准(例如多少延迟是可以接受的,我必须获得什么等)和 c)只有当你确信你在正确的轨道上时才继续。没有人能告诉你。

      希望有帮助

      【讨论】:

      • No. The type safety is guaranteed at compile time as long as you do not have any warnings. 这不是从JVM的角度来看的,所以JVM必须在运行时检查。 JVM 不知道该强制转换是来自经过良好检查的泛型、不安全的泛型(警告被抑制)、原始形式,还是来自在十六进制编辑器中编写字节码的人。它不知道,因此必须假设类型安全无法保证。
      • @yshavit 嗯,我不确定你的意思。只要您在编译时没有任何警告,编译器就会确保所有泛型类型都经过良好检查。 JVM 只需要在通常的强制转换场景中采取任何行动。它不必执行额外的检查
      • JVM 对 Java 语言一无所知。它所知道的只是字节码。此外,每当执行不正确的强制转换时,都需要抛出ClassCastException。除非它可以证明强制转换将基于字节码工作(它可能会这样做,我不确定 - 但这与编译时安全无关,因为该信息在编译时被删除),它需要在运行时检查。
      • @yshavit 我想我理解你的反对意见。您指的是在演员阵容期间执行的运行时检查,而我正在谈论由泛型使用引起的任何 附加 检查。我同意,但尚不清楚 OP 询问的两种情况中的哪一种。我将更新我的答案以澄清这一点
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-07
      • 2020-09-29
      • 2013-05-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多