13

我知道当 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 件事之一。如果您正在阅读本文是因为您确实遇到了性能问题,请帮自己一个忙,并启动一个分析器并找出瓶颈在哪里。如果你真的相信它的类型转换,那么你才应该尝试以某种方式优化它。

4

4 回答 4

15

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

给定以下类:

// 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        
}

正如你可以在 offset中一样14,检查结果的cell.get()类型以验证它确实是一个字符串:

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

这确实会导致一些运行时损失。然而,由于 JVM 已经得到了很好的优化,其影响可能很小。

更长的故事:

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

底线:不要过早优化。

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

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

于 2013-07-19T17:57:12.557 回答
1

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

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

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

List list = new ArrayList();

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

于 2013-07-19T17:25:46.390 回答
0

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

是的。本教程中简要介绍了类型擦除

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

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

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

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

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

会让我自己的CheesecakeList班级看起来与ArrayList...相同

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

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

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

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

希望有帮助

于 2013-07-19T18:04:29.160 回答
0

如果您获取 JDK 源代码ArrayList,并将其克隆到CheesecakeList中,并将所有Object出现的 地方更改ArrayListCheesecake,则结果(假设您正确执行)将是一个您可以在应用程序中使用并且需要较少checkcast操作的类,vs使用ArrayList<Cheesecake>.

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

于 2013-07-19T18:10:46.303 回答