问题 Java Generics的类型擦除会导致完全类型转换吗?


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

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

  • 字节码是否包含类型转换?
  • 如果是,是否包括运行时检查以检查其类型是否正确?
  • 转换本身是否需要非常重要的时间?
  • 会做我自己的 CheesecakeList 看起来与...相同的类 ArrayList,只是与所有的 Objects转向 Cheesecake我会获得任何东西(再次只是一个假设,我对由于更多的类而没有更大的二进制文件的任何副作用感兴趣,也不感兴趣。)

我对任何其他类型擦除造成的问题都不感兴趣,只是一个比我更了解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); 
}

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


1245
2017-07-19 17:20


起源



答案:


短篇小说:是的,有一种类型检查。这是证明 -

鉴于以下类别:

// 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 类?这个类不会定义一个数组来保存元素吗?请注意,对阵列的每个任务都会产生隐藏的类型检查。所以你不会获得你想象的那么多(尽管你的程序可能会执行比写操作更多的读操作,所以a Cheesecak[] 数组会给你一些东西)。

底线:不要过早优化。

最后评论。人们通常认为类型擦除意味着 Cell<String> 编译成 Cell<Object>。这不是真的。擦除仅适用于泛型类/方法的定义。它不适用于这些类/方法的使用地点。

换句话说,班级 Cell<T> 被编译为好像它被写成 Cell<Object>。如果 T 有一个不受限制的(比如说 Number)然后它被编译成 Cell<Number>。在代码中的其他位置,通常是变量/参数,其类型是实例化的 Cell 上课(如 Cell<String> myCell),擦除不适用。这个事实 myCell 是类型 Cell<string> 保存在类文件中。这允许编译器正确地检查程序。


15
2017-07-19 17:57



“底线:不要过早优化。” 这个。 +1 - Jan Doerrenhaus
这是一个纯粹的教育问题。但你的说法是说类型转换存在并且JVM处理得非常好,这是我想从这个问题得到的两个主要内容。 - James J. Regan IV
值得注意的是,运行时检查是CPU的分支预测能够非常容易地处理的,这将进一步提高性能。 - yshavit


答案:


短篇小说:是的,有一种类型检查。这是证明 -

鉴于以下类别:

// 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 类?这个类不会定义一个数组来保存元素吗?请注意,对阵列的每个任务都会产生隐藏的类型检查。所以你不会获得你想象的那么多(尽管你的程序可能会执行比写操作更多的读操作,所以a Cheesecak[] 数组会给你一些东西)。

底线:不要过早优化。

最后评论。人们通常认为类型擦除意味着 Cell<String> 编译成 Cell<Object>。这不是真的。擦除仅适用于泛型类/方法的定义。它不适用于这些类/方法的使用地点。

换句话说,班级 Cell<T> 被编译为好像它被写成 Cell<Object>。如果 T 有一个不受限制的(比如说 Number)然后它被编译成 Cell<Number>。在代码中的其他位置,通常是变量/参数,其类型是实例化的 Cell 上课(如 Cell<String> myCell),擦除不适用。这个事实 myCell 是类型 Cell<string> 保存在类文件中。这允许编译器正确地检查程序。


15
2017-07-19 17:57



“底线:不要过早优化。” 这个。 +1 - Jan Doerrenhaus
这是一个纯粹的教育问题。但你的说法是说类型转换存在并且JVM处理得非常好,这是我想从这个问题得到的两个主要内容。 - James J. Regan IV
值得注意的是,运行时检查是CPU的分支预测能够非常容易地处理的,这将进一步提高性能。 - yshavit


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

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

然后可以在编译时检查泛型类型。这将抹去:

List list = new ArrayList();

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


1
2017-07-19 17:25



我知道,我没有得到的是多少成本。还有,当我这样做的时候 list.get(0).Eat(me) 它必须编译到 ((Cheesecake)list.get(0)).Eat(me) 是演员(可能/最有可能发生在循环中)是重要的 - James J. Regan IV
我不确定成本是多少(我猜想上传的零成本)。但它肯定不比昂贵 Object o = new Integer(5);。 - Oliver Charlesworth
即使有成本,它也会大大超过不使用泛型时可能获得的运行时异常 - tom
但是第二个例子很有意思,因为你是对的,这有一个隐含的下线。我不知道如何处理。 - Oliver Charlesworth
虚拟方法调度可能会花费更多。它都经过了大量优化。擦除运行时类型的一个优点是“通用”对象不需要携带尽可能多的类型信息。 - Tom Hawtin - tackline


如果您使用了JDK源代码 ArrayList,你克隆了它 CheesecakeList,并改变了所有 Object 发生在 ArrayList 至 Cheesecake,结果(假设你做得正确)将是一个你可以在你的应用程序中使用并且需要少一个的类 checkcast 操作,使用 ArrayList<Cheesecake>

那说,一个 checkcast 操作非常快,特别是如果您正在检查指定类的对象而不是子类。对于略微扰乱JITC优化而言,它的存在可能比实际执行成本更重要。


0
2017-07-19 18:10





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

是。简要描述了类型擦除 本教程

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

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

[编辑: 澄清“否”意味着没有 额外 由于使用泛型而由编译器插入的运行时检查。仍在执行铸造期间的任何运行时检查]

转换本身是否需要非常重要的时间?

通过转换你的意思是铸造?如果是的话,即使你没有泛型也会出现这种情况

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

一般来说,这不是一个简单的问题,当然也不会考虑其他因素。首先,我们不再讨论泛型。你问的是一个专门的课程是否比一个快 ArrayList可以想像 是的,有一个特殊的类可以避免强制转换应该更快。但真正的问题是 你应该关心它吗?

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

另外不要忘记编译器的发展。未来的版本可能会有一个优化,可以自动修复性能问题(如果你可以使用它,那就是),而如果你坚持使用一个无用的代码库,它可能会留在项目中,只要它存在。

所以我的建议是:a)确定你在这个特定领域遇到问题,否则不要试图超越编译器或语言本身b)为你的代码计时,设定你想要实现的标准(例如延迟多少)是可以接受的,我必须得到的东西等)和c)只有当你确信自己走在正确的轨道上时才进行。没有人能告诉你。

希望有所帮助


0
2017-07-19 18:04



No. The type safety is guaranteed at compile time as long as you do not have any warnings. 它不是从JVM的角度来看的,所以JVM必须在运行时检查。 JVM不知道该转换是来自经过良好检查的泛型,不安全的泛型(警告被抑制),原始表单,还是某人在十六进制编辑器中编写字节码。它不知道,因此必须假设不保证类型安全。 - yshavit
@yshavit嗯我不确定你是什么意思。只要您在编译时没有任何警告,编译器就会确保所有泛型类型都经过良好检查。 JVM只需要采取在通常的强制转换场景中执行的任何操作。它不必执行额外的检查 - c.s.
JVM对Java语言一无所知。所有它知道的是字节码。此外,它需要抛出一个 ClassCastException 每当进行不正确的演员表演时。除非它可以证明转换将基于字节码工作(并且它可能会这样做,我不确定 - 但这与编译时安全性无关,因为该信息在编译时被擦除),它需要在运行时检查。 - yshavit
@yshavit我想我理解你的反对意见。您正在讨论在演讲期间执行的运行时检查,而我正在讨论任何问题 额外 检查由泛型使用引起的。我同意虽然目前尚不清楚OP要求的两个案例中的哪一个。我会更新我的答案以澄清这一点 - c.s.