Java泛型的类型擦除是否会导致完全类型转换



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

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

  • 字节码是否包含类型转换
  • 如果是,它是否包括运行时检查,以检查其类型是否正确
  • 转换本身是否需要相当长的时间
  • 如果让我自己的CheesecakeList类看起来与ArrayList完全相同,只是让所有的Object变成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); 
}

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

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

给定以下类别:

// 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<String>被编译成Cell<Object>。这不是真的。擦除仅适用于泛型类/方法的定义。它不适用于这些类/方法的使用站点。

换句话说,类Cell<T>被编译,就好像它被写为Cell<Object>一样。如果T具有上界(比如Number),则它被编译为Cell<Number>。在代码的其他地方,通常是类型为Cell类(如Cell<String> myCell)的实例化的变量/参数,不应用擦除。myCell属于Cell<String>类型这一事实保留在类文件中。这允许编译器正确地键入检查程序。

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

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

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

List list = new ArrayList();

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

如果您将ArrayList的JDK源代码克隆到CheesecakeList中,并将ArrayList中出现的所有Object更改为Cheesecake,则结果(假设您做得正确)将是一个可以在应用程序中使用的类,并且与使用ArrayList<Cheesecake>相比,只需少执行一次checkcast操作。

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

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

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

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

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

[编辑:澄清"否"意味着编译器没有因为使用泛型而插入的额外运行时检查。强制转换期间的任何运行时检查仍在执行]

转换本身是否需要相当长的时间?

你所说的转换是指铸造吗?如果是,那么即使你没有仿制药,情况也是如此

会让我自己的CheesecakeList类看起来和ArrayList完全一样吗。。。

总的来说,这不是一个简单的问题,当然也不能不考虑其他因素。首先,我们不再讨论泛型了。你在问一个专业课是否会比ArrayList更快假设是的,拥有一个可以避免施法的特殊类应该会更快。但真正的问题是你应该关心它吗?

要回答最后一个问题,你必须具体。每个案例都不一样。例如,我们谈论的对象有多少?数百万?重复呼叫?额外的时间在节目的表演中值得注意吗?你计时了吗?我们期望我们的数据在未来如何扩展?等等

也不要忘记编译器是进化的。未来的版本可能会进行优化,自动解决性能问题(如果你能接受它的话),而如果你坚持使用一个无用的代码库,它可能会在项目存在的时间内一直存在。

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

希望对有所帮助

最新更新