什么是具体化的泛型?它们如何解决类型擦除问题,为什么不能在不进行重大更改的情况下添加?

89

我读过Neal Gafter在这个主题上的博客,但仍不清楚其中的一些要点。

为什么在当前Java、JVM和现有集合API的状态下无法创建保留类型信息的Collections API实现?它们不能在将来的Java版本中替换现有实现,以确保向后兼容性吗?

举个例子:

List<T> list = REIList<T>(T.Class);

REIList是这样的:

public REIList<T>() implements List {
  private Object o;
  private Class klass;

  public REIList(Object o) {
    this.o = o;
    klass = o.getClass();
  }
... the rest of the list implementation ...

方法使用 Object o 和 Class klass 来获取类型信息。

为什么保留泛型类信息需要语言更改而不是仅仅进行 JVM 实现更改呢?

我没有理解到什么?

5个回答

47

泛型的本质在于,具体化泛型在编译器中有保留类型信息的支持,而类型擦除则没有。据我所知,最初引入类型擦除的主要目的是为了实现向后兼容(例如,低版本的JVM仍然能够理解泛型类)。

你可以在实现中显式添加类型信息,正如你上面所做的那样,但这需要每次使用列表时添加额外代码,而且在我看来非常混乱。此外,在这种情况下,除非自己添加检查,否则所有列表方法都没有运行时类型检查,然而具体化泛型将确保运行时类型。


8
较旧JVM的源代码和目标代码可以在新的JVM上运行,无需进行任何更改(源代码可能会使用“enum”作为标识符而出现问题,但这并不是泛型本身的问题)。 - Tom Hawtin - tackline
2
当您尝试在旧版本的JRE/JVM中运行使用较新版本JDK编译的程序时,可能会出现UnsupportedClassVersionError。因此,Tom Hawtin和Richard Gomes所说的事实应该是相反的,即较低版本的JVM仍然可以理解通用类。 - blackr1234

22
与大多数Java开发人员的观点相反,尽管在非常有限的情况下,保留编译时类型信息并在运行时检索此信息是可能的。换句话说:Java以非常有限的方式提供了具体化的通用性
关于类型擦除,请注意,在编译时,编译器具有完整的类型信息,但在生成二进制代码时,故意删除此信息通常,这是一种称为类型擦除的过程。之所以这样做,是由于兼容性问题:语言设计者的意图是在版本之间提供完全的源代码兼容性和完全的二进制代码兼容性。如果实现方式不同,当您迁移到较新版本的平台时,必须重新编译旧应用程序。它的实现方式是,所有方法签名都被保留(源代码兼容性),您不需要重新编译任何内容(二进制兼容性)。
关于Java中的具体化泛型,如果需要保留编译时类型信息,则需要使用匿名类。重点是:在非常特殊的情况下,即匿名类的情况下,可以在运行时检索完整的编译时类型信息,这又意味着:具体化的泛型。
我写了一篇关于这个主题的文章:

http://rgomes-info.blogspot.co.uk/2013/12/using-typetokens-to-retrieve-generic.html

在这篇文章中,我描述了我们的用户对这种技术的反应。简而言之,这是一个晦涩的主题,对大多数Java开发人员来说,这种技术(或者模式,如果你更喜欢这个词)看起来很陌生。

示例代码

上述文章中提到了链接到实践该想法的源代码。


12
这与具体化的泛型无关。 是的,在类声明中的泛型,如泛型参数的类型、字段类型、方法签名中的类型、超类类型、内部类包含类的类型(匿名类没有特殊之处),会存储在字节码中,因为使用此类并且只有.class文件的其他类需要知道这些信息来强制实施泛型。然而,对于泛型类型的运行时对象的具体化和非具体化讨论,与您上面所说的内容无关。 - newacct
1
@newacct:是的,它确实可以。是的,从保留可以在运行时检索到的编译时信息的角度来看,内部类是特殊的。根据定义,当您可以在运行时从泛型类中检索编译时类型信息时...您拥有什么? - Richard Gomes
7
您还可以同样地检索任何类的编译时信息,例如超类、超接口、字段和方法等。 - newacct
11
不,您只是检索其超类所扩展的通用类型参数。匿名类创建表达式创建一个类的实例,该类是您给出的类型的子类。final K k1 = new K<java.lang.Double>() {};与本地类class Foo extends K<java.lang.Double> {}; final K k1 = new Foo();完全相同。 - newacct
2
这与问题毫不相关。问题是为什么OP描述的具体实现需要语言更改而不仅仅是JVM更改。你的回答与两者都无关。 - user207421
显示剩余4条评论

16
据我所知(并根据链接),Java泛型只是使用对象集合并进行前后转换的现有技术的语法糖。使用Java泛型更安全和简单,因为编译器可以检查并验证在编译时是否保持类型安全性。然而,运行时却是一个完全不同的问题。
另一方面,.NET泛型实际上创建了新类型——在C#中,List<String>List<Int>是不同的类型。在Java中,它们在底层是相同的东西——一个List<Object>。这意味着如果你有一个List<String>和一个List<Int>,你不能在运行时看到它们被声明为什么类型,只能看到它们现在是什么类型。
重新实例化泛型可以改变这一点,给Java开发人员提供与.NET现有功能相同的能力。

2
我将您的List<*>放在反引号中,以便类型显示出来。 - David Z
@ David - 谢谢。我已经为别人做过这件事很多次了,你认为我应该记住... - Harper Shelby
1
您可以使用匿名类来模拟具体化的泛型,即在此情况下类型信息可在运行时获得。尽管可以这样做,但其适用性非常有限且技术非常棘手。具体化的泛型是一个很大的缺失;这是确实如此的。 - Richard Gomes

4

我对这个问题并不是很精通,但据我所知,在编译时类型信息就已经丢失了。与 C++ 不同,Java 没有使用模板系统,类型安全完全通过编译器实现。在运行时,List 实际上始终是 List。

因此,我的看法是需要改变语言规范,因为类型信息对 JVM 不可用 因为它不存在


这在大多数情况下是正确的,除非你有匿名类。我的意思是:当你使用匿名类时,你可以避免类型擦除。 - Richard Gomes
更具体地说,Java不会像C++对模板那样生成代码的多个副本。Java在调用方站点上生成一个单一的对象。因此,尽管在匿名类的情况下运行时类型信息是可用的,但你只能得到这些信息本身。字节码不会针对你将在运行时传递的实际类型进行目标定位。 - Richard Gomes

0
我能想到的简单解释是,他们故意选择了简洁而不是代码膨胀,因为在编译后擦除之后,泛型类及其子类型只有一个实现。
另一方面,C++为每个实现创建单独的代码版本。

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