【问题标题】:Why not remove type erasure from the next JVM?为什么不从下一个 JVM 中删除类型擦除?
【发布时间】:2016-06-27 13:56:19
【问题描述】:

Java 在 Java 5 中使用泛型引入了类型擦除,因此它们可以在旧版本的 Java 上工作。这是兼容性的权衡。我们已经失去了兼容性[1][2] [3]——字节码可以在更高版本的 JVM 上运行,但不能在更早的版本上运行。这看起来可能是更糟糕的选择:我们丢失了类型信息,并且我们仍然无法在旧版本上运行为新版本 JVM 编译的字节码。发生了什么?

具体来说,我想问是否有任何技术原因导致无法在 JVM 的下一个版本中删除类型擦除(假设,像以前的版本一样,它的字节码无论如何都无法在最后一个版本上运行)。

[3]:对于那些真正喜欢它的人,类型擦除可以以类似于 retrolambda 的方式向后移植。

编辑:我认为关于向后与向前兼容性定义的讨论模糊了这个问题。

【问题讨论】:

  • 向后兼容性从未丢失。旧的 Java 1.1 程序很可能仍然可以在 JRE 8 VM 上顺利运行,这就是链接帖子所说的。
  • 不完全一样,也可以参考stackoverflow.com/questions/4692626/…
  • @Prime - 但这并不是 Java 上下文中向后兼容性的含义。
  • Java 8 向后兼容为旧版 Java 编写的代码。您可能认为您的代码与未来的 JRE 向前兼容,但实际上是那些未来的 JRE 与您的旧代码向后兼容。您的问题表明对 Java 中向后兼容的含义缺乏理解。
  • @Prime 你是对的,海报是错的。您编写的代码是向前兼容的。它在 JVM 8、9、10、11 上编译和运行……生成的字节码不向后兼容。一世。 e 你不能在 jvm 5,4,3,2 上运行它。 JVM 本身是向后兼容的,因为它可以运行 JVM 5、4、3、2 代码。 JDK 也是如此,因为它支持旧代码的编译。类型错误是一个已知问题,正在进行研究和讨论以更改或删除它。

标签: java jvm backwards-compatibility type-erasure


【解决方案1】:

类型擦除不仅仅是您可以打开或关闭的字节码功能。

它会影响整个运行时环境的工作方式。如果您希望能够查询泛型类的每个实例的泛型类型,则意味着为泛型类的每个对象实例创建元信息,类似于运行时Class 表示。

如果您编写new ArrayList<String>(); new ArrayList<Number>(); new ArrayList<Object>(),您不仅会创建三个对象,还可能会创建三个反映这些类型的额外元对象,ArrayList<String>ArrayList<Number>ArrayList<Object>,如果它们以前不存在的话。

考虑到在典型应用程序中使用了数千个不同的List 签名,其中大多数从未在需要此类反射可用性的地方使用(由于缺少此功能,我们可以得出结论:目前,它们都可以在没有这样的反射的情况下工作)。

当然,这会乘以一千种不同的泛型列表类型,这意味着一千种不同的泛型迭代器类型、一千种拆分器和流化身,甚至不计算实现的内部类。

它甚至会影响没有对象分配的地方,这些地方目前正在探索引擎盖下的类型擦除,例如Collections.emptyList()Function.identity()Comparator.naturalOrder() 等在每次调用时都返回相同的实例。如果您坚持让特定捕获的泛型类型可反射检查,这将不再有效。所以如果你写

List<String> list=Collections.emptyList();
List<Number> list=Collections.emptyList();

您将必须收到两个不同的实例,每个实例在 getClass() 或未来的等价物上报告不同的实例。


看起来,希望获得这种能力的人对他们的特定方法有一个狭隘的看法,如果他们能够反思地找出一个特定参数是否实际上是两种或三种类型中的一种,那就太好了,但永远不要考虑携带有关数千个泛型类的潜在成百上千个泛型实例化的元信息的权重。

这是我们必须询问我们得到什么回报的地方:支持有问题的编码风格的能力(这就是由于通过反射找到的信息而改变代码行为的全部内容)。


到目前为止的答案只解决了删除类型擦除的简单方面,即内省实际实例类型的愿望。实际实例具有具体类型,可以报告。正如user the8472 中的this comment 中所提到的,删除类型擦除的需求通常还意味着希望能够强制转换为(T) 或通过new T[] 创建数组或通过以下方式访问类型变量的类型T.class.

这将引发真正的噩梦。类型变量与具体实例的实际类型不同。类型变量可以解析为 a,例如? extends Comparator&lt;? super Number&gt; 举一个(相当简单的)例子。提供必要的元信息意味着不仅对象分配变得更加昂贵,每个方法调用都可能带来这些额外的成本,甚至更大,因为我们现在不仅在讨论泛型类与实际类的组合,而且也包括所有可能的通配符组合,甚至是嵌套的泛型类型。

请记住,类型参数的实际类型也可以引用其他类型参数,从而将类型检查变成一个非常复杂的过程,如果您允许创建一个数组出来,每次存储操作都要重复一遍。

除了严重的性能问题之外,复杂性还引发了另一个问题。如果您查看javac 的 bug 跟踪列表或 Stackoverflow 的相关问题,您可能会注意到该过程不仅复杂,而且容易出错。目前,javac 的每个次要版本都包含有关泛型类型签名匹配的更改和修复,影响将被接受或拒绝的内容。我很确定,您不希望像类型转换、变量赋值或数组存储这样的内在 JVM 操作成为这种复杂性的受害者,对每个版本中的合法或不合法有不同的想法,或者突然拒绝 javac由于规则不匹配而在编译时接受。

【讨论】:

  • 您假设它只能通过反射使用。但未擦除的泛型将意味着类中的类型变量(例如 T)将是真实的,即通过 (T) 进行转换将是实际的转换并提供快速失败的行为,new T[] 将创建一个数组 T.class 可以给你一个类对象。此外,虽然您是正确的,需要创建元数据对象,但它们仍然只是类数量的一个常数因素,因为通用签名最终由分配调用点驱动,每个类只有有限数量的分配调用点。
  • @the8472:我专注于 easy 部分,因为您所描述的内容让事情变得更糟。由于还有通用的方法,您描述的功能意味着每个调用都可以承受这种开销,而不仅仅是对象分配站点。我用我的答案来解决一些相关的问题。当然,呼叫点的数量也是有限的,但我们宇宙中的恒星数量可能也是有限的……
  • @Raphael getGenericType() 只会为您提供声明,而不是实例化的实际类型。可以使用反射发现typejava.util.List的声明是List&lt;T&gt;;这并没有说明数千个列表实例的实际参数化。可以查询变量的泛型类型,即字段和参数(不是局部变量),但是同一个对象可以被几十个不同类型的变量引用;对象本身没有泛型类型。如答案中的示例所示。
  • @Eugene 首先,泛型根本不会改变编译代码的工作方式。抛开反射不谈,您可以在根本不了解泛型的 JVM 上运行编译后的泛型代码。这就是为什么对于int size(List&lt;Integer&gt; list),通用签名会被存储,但是你仍然不能在同一个类中有一个方法int size(List&lt;String&gt; list),因为一个类中有两个int size(List)方法是被禁止的。
  • @ggb667 只在少数情况下工作的类型系统不值得努力。当我编写List&lt;String&gt; l = List.of("foo", "bar"); 时,我正在调用一个generic 方法,它的实现代码实例化了一个generic List,但没有提示该类型是final 类型String。如果我改为写List&lt;Object&gt; l = List.of("foo", "bar"); 会怎样?完全相同的代码,但是现在,of 方法中的代码应该神奇地确定这 不是 具有 final 元素类型的列表?魔法应该传播多少级? stream.map(f).collect(toList()), final 输入与否?
【解决方案2】:

project valhalla 将来会在一定程度上删除擦除,以便为值类型启用专门的实现。

或者更准确地说,类型擦除实际上意味着泛型没有类型特化,而 valhalla 将引入对原语的特化。

具体来说,我想问是否有任何技术原因导致无法在下一版本的 JVM 中删除类型擦除

性能。您不必为泛型类型、实例或生成的类的所有组合生成专门的代码,不必携带类型标签、多态内联缓存和运行时类型检查(编译器生成的instanceof 检查)保持简单,我们仍然通过编译时检查获得大部分类型安全。

当然也有很多缺点,但已经做出了权衡,问题是什么会促使 JVM 开发人员改变这种权衡。

这也可能是一个兼容性问题,可能有代码通过依赖类型擦除来执行未经检查的强制转换以滥用泛型集合,如果强制执行类型约束,这将破坏。

【讨论】:

  • 为什么在运行时具有泛型类型参数的值意味着专业化?
【解决方案3】:

您对向后兼容性的理解是错误的。

期望的目标是 new JVM 能够正确运行 old 库代码,即使使用 new 代码也不会发生变化。这允许用户可靠地升级他们的 Java 版本,甚至升级到比编写代码的版本更新得多的版本。

【讨论】:

  • 删除类型擦除会如何阻碍这一点?旧代码不会依赖类型信息(因为它不存在),新代码只能在新的 JVM 上运行。每个人都可以无缝升级。
  • @Prime - 不会。但是,您使用向后兼容性的虚假想法作为抛弃 >>real
  • @StephenC 类型擦除是减法——如果您的程序假定类型信息已被擦除,那么只要类路径中有 java.util.List[Object] 就可以正常运行。添加更多类型信息可能意味着添加专业化,因此 JVM 会知道例如java.util.List[Integer] 并且可以避免运行时类型检查。可以通过使用具体类型在运行时获取此信息(就像 scala 所做的那样,请参阅docs.scala-lang.org/overviews/reflection/…)。就像您始终可以在编译时获得可用信息一样...
  • 运行时的时间,如果你有足够的决心(在 scala 的情况下,让编译器为你添加它)。我不确定这是否是一个 的想法,但我的问题是这样做是否存在任何技术障碍。迄今为止给出的答案在这方面很有启发性。
猜你喜欢
  • 2020-01-09
  • 2016-04-21
  • 2022-11-27
  • 1970-01-01
  • 2017-07-20
  • 2012-03-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多