Java泛型在何时需要在运行时进行类型转换?

6
我是一名有用的助手,可以为您翻译文本。
我正在阅读关于C++模板和C#泛型的讨论,以及它们与Java的类型擦除泛型的区别。我看到了一条声明,称Java仍然在运行时使用强制转换,例如当处理集合时。如果这是真的,那我不知道!
假设我有如下代码:
ArrayList<SomeClass> list = new ArrayList<SomeClass>();
...
SomeClass object = list.get(0);

我的问题是,这是否被有效地编译为:
ArrayList list = new ArrayList();
...
SomeClass object = (SomeClass) list.get(0);

如果是这样,为什么?我认为类型为ArrayList<SomeClass>的事实可以在编译时和运行时保证只有SomeClass会存储在ArrayList中。或者您是否可以进行不安全的类型转换以将ArrayList<OtherClass> 转换成 ArrayList<SomeClass>
还有其他Java泛型在运行时进行类型转换的情况吗?
最后,如果确实在运行时进行类型转换,是否存在JIT可以省略执行时类型转换检查的情况?
(请勿回答/评论微小的优化不值得做,预防性优化是万恶之源等类似问题。我看到这些问题在其他类似的问题上。这些观点已经被充分理解,但它们并不影响理解如何在幕后实现类型擦除的重点。)

2
你有很多问题。但答案归结为:Java泛型的实现是为了改善编译时错误检查。 - ControlAltDel
这个问题已经被Oracle(Sun)提供的各种泛型信息和教程所覆盖。为了保持向后兼容性,泛型被作为编译时特性添加进来。http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf - Brian Roach
SLaks、Richante和Adriano的回答都让我有所启发。最重要的是@user1291492提到的,泛型主要用于编译时错误检查。目前还不太清楚JIT的作用。而且我仍然认为,如果Java的语义不允许不安全的转换(例如将ArrayList<String>转换为ArrayList<Integer>),那么你可以优化运行时检查,但这是另一个问题的话题。我选择Richante的答案作为“正确”的答案,因为他付出了编译和反汇编的努力(谢谢!)。 - Max
@Max,仅仅因为Java字节码包含了强制类型转换并不意味着它通常不能在运行时被JIT优化掉,供你参考。 - Louis Wasserman
@LouisWasserman,当然,我的意思是没有人最终回答了这个问题:JIT 何时优化掉这些检查。 - Max
公平的说,尽管实时编译器通常被视为“不可预测、深奥和难以理解的魔法”。 - Louis Wasserman
5个回答

5
你的假设是正确的。
类型检查始终是必要的,因为您可以编写以下合法代码:
ArrayList<X> good = new ArrayList<X>();
ArrayList q = x;
ArrayList<Y> evil = (ArrayList<Y>)q;  //Doesn't throw due to type erasure
evil.add(new Y()); //this will actually succeed
X boom = good.get(0);
< p > 从 ArrayList 转换为 ArrayList<Y> 会 (总是) 引发未检查的类型转换警告,但在运行时也 (总是) 会成功。 < /p >

你如何只用5个字符留下评论 - 它总是让我写至少15个字符! - ControlAltDel
1
@user1291492:用户名计入字符计数。 - SLaks

5

这是我写的一个简短的程序:

public class Test<T> {

  public T contents;

  public Test(T item) {
    contents = item;
  }

  public static void main(String[] args) {

    Test<String> t = new Test<String>("hello");
    System.out.println(t.contents);

  }

}

尝试使用javac编译它,然后使用javap -verbose查看字节码。我选择了一些有趣的行:

public java.lang.Object contents;

这段文字应该出现在Test构造函数的定义之前。在示例代码中,它是类型T,现在变成了Object。这就是擦除。

现在,看一下主方法:

public static void main(java.lang.String[]);
Code:
   Stack=3, Locals=2, Args_size=1
   0:   new #3; //class Test
   3:   dup
   4:   ldc #4; //String hello
   6:   invokespecial   #5; //Method "<init>":(Ljava/lang/Object;)V
   9:   astore_1
   10:  getstatic   #6; //Field java/lang/System.out:Ljava/io/PrintStream;
   13:  aload_1
   14:  getfield    #2; //Field contents:Ljava/lang/Object;
   17:  checkcast   #7; //class java/lang/String
   20:  invokevirtual   #8; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   23:  return

然后我们可以看到第17行的checkcast命令,就在println之前 - 这是Java将Object强制转换为擦除的泛型类型String的地方。


1

请记住,在运行时,ArrayList<String>ArrayList 相同,并使用底层的 Object[] 实现。JIT通常足够聪明,可以省略转换,但由于泛型在运行时被擦除,因此转换是必要的。


1

Java在维护版本兼容性方面做得非常好,这意味着Java5+语法可以编译成1.4兼容的类。我想这就是为什么ArrayList不能保证只有SomeClass存储在其中的原因之一。

如果想要查看泛型代码真正编译成什么,请使用优秀的JAD工具。

您的代码确实编译成了SomeClass object = (SomeClass) list.get(0);


1

我认为,类型为ArrayList的列表保证了在编译时和运行时只有SomeClass会被存储在ArrayList中,是吗?或者你可以进行不安全的类型转换将ArrayList转换为ArrayList吗?

是的,在运行时会丢弃类型信息。

这意味着您无法确定您是否拥有类型为ArrayList<String>或类型为ArrayList<Long>的对象。JVM不知道泛型类实例的实际类型参数,因此您没有运行时检查(并且只有在使用该数据时才会看到错误)。

以下代码是有效的:

public void enqueueItem(ArrayList<Integer> list, Integer item) {
    List listAlias = list;
    listAlias.add(x.toString());
}

在运行时,这不是一个问题,类型检查仍然存在,可以帮助您避免任何问题,并且您可以将旧代码与新代码进行互操作。即使在.NET环境中,您也具有相同的行为(IList<T>派生自IList)。

您所付出的只是强制转换的性能影响(这是与Java的泛型实现的重大区别)。更多或更少地说,泛型是一种编译时的辅助工具,用于检测错误并使您的代码更易读。例如,对于编译器来说,此代码非常有效:

public ArrayList getCollection() {
   return new ArrayList<Integer>();
}

public void doStuff() {
   ArrayList<String> list = getCollection();
   list.add("text");
}

Java泛型中是否存在其他需要在运行时进行类型转换的情况?
是的,每次使用它们时都会进行类型转换(如果您在泛型中做了一些不好的事情,这就是会出现错误的地方)。
最后,如果确实使用了运行时转换,是否有时JIT可以省略运行时转换检查?
我认为不会,它总是会进行转换,但这是一个非常具体的实现细节,它跟踪子类的类型参数,因此如果您从泛型类派生自己的类,则可以避免此问题。
如果是这样,为什么?
所有现有的代码都将与新的泛型一起工作,即使您更改了某些接口。这意味着您可以逐步更新到泛型。我不担心性能影响(我们已经很长时间生存下来了,现在有问题吗?),但我不喜欢所有这些技巧。也许,毕竟,“我们不能同时拥有满树的橡树和醉妻”。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接