我们如何获得我们的[Java]泛型

2021-04-01 20:06:27

在我们谈论泛型的地方之前,我们首先必须谈谈他们所在的位置,以及他们如何到达那里。本文档将主要关注我们现在到达的泛型,以及为什么为为现在的泛型方式设置基础的手段,我们现在将影响我们尝试构建的“更好”的泛型。

特别是,我们强调,擦除实际上是在2004年向Java添加仿制机的明智和务实的选择 - 以及让我们选择通过擦除选择翻译的许多力量可能仍然可以在今天运营。

向任何关于Java泛型的开发人员询问,您可能会生气(虽然通常不知情)咆哮。删除可能是Java最广泛而深入的误解概念。

擦除不是Java,也不是泛型;它是一种普遍存在的,通常需要的工具,用于将一个级别转换为较低级别(例如从Java源编译到字节码时,或编译C源代码到本机代码。)这是因为我们从堆栈中移动高级语言到中间表示到原始代码到硬件,由较低级别提供的类型抽象几乎总是更简单,弱于更高级别的 - 而且正确地弱。 (我们不希望将虚拟调度的语义释放到X86指令集中,或模拟其寄存器中的Java的基本类型集。)擦除是在一个级别映射更丰富的类型,以在较低级别的较小类型(理想情况下,在更高级别执行声音类型检查后),并且每天都是编译器。

例如,Java字节码集包含用于在堆栈和本地变量集之间移动整数值(IADD,IMUL等)之间移动整数值的指令(IADD,IMUL等)浮动的相似说明(浮灯,FSTORE ,fmul等),龙(lload,lstore,lmul),双打(dload,dstore,dmul)和对象引用(Aload,Astore。),但没有这样的字节,短裤,字符或布尔值的说明 - 因为这些类型由编译器删除到INT,并使用INT移动和算术指令。这是一个务实的设计权衡,用于设计字节码指令集;它降低了指令集的复杂性,从而可以提高运行时的效率。 Java语言的许多其他功能(例如,检查的异常,方法重载,枚举,明确分配分析,嵌套类,Lambdas或本地类别的临界类别捕获)是“语言小说” - 在Java编译器中检查它们但在翻译到Classfiles中。

类似地,当编译C到原生代码时,签名和无符号int都被删除到通用寄存器中(没有单独的符号VS无符号寄存器),并且CONST变量存储在可变寄存器和存储器位置。我们根本没有找到这种擦除怪异。

用参数多态性翻译语言的通用类型有两种常见的方法 - 同质和异质翻译。在同质的翻译中,通用类foo< t>被翻译成单个工件,例如foo.class(对于通用方法也相同)。在异构的翻译中,每次实例化通用类型或方法(foo< string> foo< integer>)被视为单独的实体,并生成单独的伪像。例如,C ++使用异构翻译:模板的不同实例是完全不同的类型,具有不同的语义和不同生成的代码。类型矢量< int>和矢量<浮动>是单独的类型。一方面,这对于类型的安全性很好(每个实例化可以单独键入膨胀后校验),并且对于所生成的代码的质量(每个实例化可以单独优化)。另一方面,这意味着较大的代码占用(自矢量< int>和向量< float>有单独的代码),我们不能谈论“某种东西的向量”(作为Java通过通配符),因为每个实例化都是一个完全的不相关的类型。 (作为可能的足迹成本的极端演示,Scala使用@specialized注释试验,当应用于型变量时,使编译器发出所有原始类型的专用版本。这听起来很酷,但导致9 n爆炸生成的类,其中n是类中专用类型变量的数量,所以人们可以从几行代码中轻松生成100MB JAR文件。)

同质化和异构翻译的选择涉及使得各种权衡语言设计师一直在制作。异构翻译提供更多类型的特异性,以更高的静态和动态足迹,并且在运行时分享较少 - 所有这些都具有性能影响。均匀翻译更适合抽象的类型的类型类型,例如Java的通配符或C#的声明站点方差(其中C ++缺乏,在载体< int&gt之间没有常见的情况下。和向量< float>)有关翻译策略的更多信息,请参阅此有影响力的纸张。

Java使用同意翻译翻译泛型。泛型是在编译时键入的,但随后是一个泛型类型,如列表< string>生成字节码时被删除到列表,以及诸如< t的键入变量延长对象>被擦除到他们绑定的擦除(在这种情况下,对象)。

类盒< t> {私人t t;公共T(t t){this.t = t;公共箱子< t>复制(){返回新框<(t); public t t(){返回t; }}

javac编译器发出单个类文件框.Class,它用作框的所有实例的实现 - 包括通配符(框<?>)和原始类型(框)。擦除字段,方法和超级性描述符;类型变量删除到其界限,泛型类型被删除到它们的头部(列表< string>删除列表),如下所示:

类盒{私有对象T;公共盒子(对象t){this.t = t; }公共框副本(){返回新框(t); public对象t(){return t; }}

保留通用签名(在签名属性中),以便编译器可以在读取类文件时看到通用签名,但JVM仅使用链接中的删除描述符。该转化方案意味着在ClassFile级别,框的布局和API都是框< t>被删除了。在使用网站上,发生同样的事情:对框的引用< string>被擦除到框,用合成铸造到使用网站的字符串。

正是在这一点上,令人诱人令人兴奋,并声明这些是明显愚蠢或懒惰的选择,或者擦除是一个肮脏的黑客。毕竟,编译器为什么会丢弃完全好类型的信息?

为了更好地了解这个问题,我们还应该问:我们是否refify那种类型的信息,我们希望与它有什么关系,以及与此相关的成本是多少?我们可以使用requied类型参数信息设想有几种不同的方式:

反射。对于一些,“Reified Generics”仅仅意味着您可以询问列表列表,无论是使用instanceof或类型变量匹配的语言工具,还是使用反射库询问类型参数。

布局或API专业化。以一种具有原始类型或内联类的语言,可能很好地平整对的布局< int,int>持有两个INT,而不是两个对盒装对象的引用。

运行时类型检查。当客户端尝试将整数放入列表< string> (通过说,一个原始列表参考),它会导致堆污染,捕获这一点并在堆污染的观点来看是很好的,而不是(也许)当它击中合成时投。

虽然不是相互排斥的,这三种可能性(反射,专业化和类型检查)有助于不同的目标(程序员便利性,性能和安全性) - 具有不同的影响和成本。虽然很容易说“我们想要重新化”,如果我们深入钻取,我们将找到重要的分歧,这是最重要的,以及它们的相对成本和福利。

要了解擦除是如何在这里的明智和务实的选择,我们也必须了解目标,优先事项和限制以及当时的替代品。

必须以二进制兼容和源兼容的方式逐渐发展现有的非泛型类。

这意味着,现有的客户端和子类(例如,ArrayList)可以继续重新编译,而不会对生长的ArrayList< t>而且现有类文件将继续链接到生成的ArrayList< t&gt的方法。支持这意味着生长类的客户端和子类可以选择立即,稍后或从未使用,并且可以独立于其他客户端或子类选择的维护者。

如果没有此要求,生成类将需要一个“旗帜日”,其中所有客户端和子类必须至少重新编译,如果没有修改 - 同时所有。对于诸如ArrayList等核心类,这主要需要世界上的所有Java代码一次重新编译(或者永久被降级以保持在Java 1.4上。)由于整个Java生态系统中的这样一个“旗帜日”是问题,我们需要一个通用类型系统,可允许在不需要客户到生成的情况下致力于生成核心平台类(以及流行的第三方库)。 (更糟糕的是,它不会是一个旗帜日,但很多,因为并非如此全球所有代码都在单个原子交易中致力于。)

陈述这一要求的另一种方法是:孤儿不可接受,可以在那里孤立,或者可以将开发人员选择泛型和保留他们已经在现有代码中所做的投资之间进行选择。通过生成兼容的操作,可以保留该代码的投资,而不是无效。

厌恶“旗日”来自Java设计的重要方面:Java是单独编译和动态链接的。单独的编译意味着每个源文件都被编译为一个或多个类文件,而不是将一组源编译为单个工件。动态链接意味着基于符号信息,类之间的引用在运行时链接;如果c类调用方法void m(int x),则在c的Classfile中,我们记录我们正在调用的方法的名称和描述符((i)v),并且在链接时间我们在d中查找方法使用此名称和描述符,如果找到匹配项,则呼叫站点已链接。

这可能听起来像很多工作,而是单独的编译和动态联动电源一个Java最大的优势之一 - 您可以编译C的一个版本D并在类路径上使用不同的版本D(只要您不行) T在D.)中进行任何二进制不兼容的变化。

对动态链接的普遍承诺是允许我们在类路径上丢弃一个新的jar来更新到依赖的新版本,而无需重新编译任何内容。我们这样做,我们甚至不知道 - 但如果这停止工作,那就确实被注意到了。

在泛造造器被引入Java时,世界上已经有很多Java代码,他们的Classfiles充满了对java.util.arraylist的API的引用。如果我们无法兼容地造成这些API,那么我们必须编写新的API来替换它们,更糟糕的是,旧API的所有客户代码都会被困住,无论是永久的选择 - 要么永远停留1.4将它们重写为同时使用新的API(不仅包括应用程序代码,而是应用程序所取决于的所有第三方库。)这将使当时存在几乎所有的Java代码。

C#使其相反的选择 - 更新其VM,并使其现有的库和依赖于它的所有用户代码无效。他们可以在当时做到这一点,因为世界上有相对较少的C#代码; Java当时没有此选项。

然而,这种选择的一个结果是,它将是预期发生的,即通用类将同时具有通用和非泛型客户端或子类。这是一个福音对软件开发过程,但它对这种混合使用情况下的类型安全具有潜在的后果。

以这种方式擦除,并支持通用和非泛型客户端之间的互操作性,会产生堆污染的可能性 - 该框中存储的内容具有与预期的编译时类型不兼容的运行时类型。当客户端使用框< string>时,只要将t分配给一个字符串,都会插入投射,从而在从类型变量(框)的数据转换到世界的数据转换的点处检测到堆污染混凝土类型。在存在堆污染的情况下,这些铸件可能会失败。

堆污染可以来自非通用代码使用泛型类,或者当我们使用未经检查的投影或原始类型时伪造对错误通用类型的变量的引用。 (当我们使用未经检查的演员或原始类型时,编译器警告我们可能会导致堆污染。)例如:

盒子<弦> bs =新框<>("嗨!"); // safebox<> BQ = BS; //安全,通过亚型箱<整数> bi =(框<整数>)bq; //未选中的演员 - 警告发出indedInteger i = bi.get(); // SyntheCence中的ClassCastException for to整数

此代码中的SIN是从框中的未经检查的施法施放。框< integer&gt ;;我们必须将开发人员带到他们的单词中,指定的框确实是一个框< integer>但堆污染并没有立即捕获;只有当我们尝试使用框中作为整数的字符串时,我们才能检测到出现问题的情况。在翻译下我们有,如果我们将盒子施放到框<整数>并返回框< string>在我们用它作为一个框之前,它在一个框< string>,没有发生任何糟糕的情况(无论好坏。)在异构的翻译下,框< string>和框<整数>将有不同的运行时类型,此演员将失败。

只要我们遵守规则,该语言实际上为泛型提供了相当强烈的安全保证:

如果程序编译没有未选中或原始警告,则编译器插入的合成演员永远不会失败。

换句话说,只有在我们与非通用代码互操作时才会发生堆污染,或者当我们骗他们的编译器时。在发现堆污染的地步时,我们会得到一个干净的例外,告诉我们预期的类型以及实际发现的类型。

围绕泛型的设计选择也受到JVM实现和在JVM上运行的语言的结构的影响。虽然到大多数开发人员“Java”是一种单片实体,实际上Java语言和Java虚拟机(JVM)是​​单独的实体,每个实体都有自己的规范。 Java编译器为JVM生成ClassFiles(其格式和语义在Java虚拟机规范中奠定),但JVM会幸福运行任何有效的类文件,无论它最初来自哪种源语言来自它。通过一些计数,有200多种语言,使用JVM作为编译目标,其中一些与Java语言(例如,Scala,Kotlin)和其他语言的其他人有很多共同之处(例如,JRuby,Jython,贾克尔。)

JVM作为汇编目标的一个原因是汇编目标,即使对于与Java完全不同的语言,它提供了一个相当抽象的计算模型,从Java语言影响有限。语言和虚拟机之间的抽象层不仅适用于刺激在JVM上运行的其他语言的生态系统,还可以是JVM的独立实现的生态系统。虽然当今市场已经大大合并了,但在泛型被添加到Java时,在JVM中有十几种商业上可行的实施。 Reify Generics意味着我们不仅需要增强语言来支持泛型,还需要JVM。

虽然可能在技术上可以在技术上向JVM添加通用支持,但不仅它是许多实施者之间需要大量协调和协议的重要工程投资,但JVM上的语言生态系统也可能有一个关于REIFIZE泛型的意见。例如,如果reationation incound incouded inclation in运行时键入,则Scala(带有声明 - 站点泛型)将很高兴拥有JVM强制java(不变)泛型亚型规则?

携带这些限制(技术和生态系统)作为强大的力量,以推动我们迈向同质翻译策略,其中在编译时删除了通用类型信息。总结一下,将我们推向该决定的力量包括:

运行时成本。异构的翻译需要各种运行时成本:更高的静态和动态占用,更高的类加载成本,更高的JIT成本和代码缓存压力等。这可能会使开发人员在类型安全之间选择的位置。表现。

迁移兼容性。在允许迁移到redified泛型的时间内没有已知的翻译方案是源和二进制兼容的,创建标志日,并使开发人员在其现有代码中的相当大的投资。

运行时成本,奖金版。如果在运行时被解释为检查类型(就像在Java的Covariant阵列中的存储一样),这将具有重要的运行时影响,因为JVM必须在每个字段或阵列元素存储上执行运行时在运行时执行通用子类型检查,使用语言的通用类型系统。 (当类型是简单的列表< string>而且,当它们是MAP< super foo> super foo&gt时,这可能听起来很容易和便宜>(实际上,后来研究对通用亚型的可辨性令人怀疑)。

JVM生态系统。获得十几个JVM供应商达成协议,以及如何在运行时重新确定类型信息是一个高度可疑的主张。

交付语用学。即使有十几名JVM供应商达成一个实际工作的计划,它也会大大增加复杂性,时间表,以及已经巨大和风险的努力。

语言生态系统。像Scala这样的语言可能不满意将Java的不变性泛型刻录到JVM的语义中。同意JVM中的一系列可接受的泛语语义将再次提高复杂性,时间表和风险危险的努力。

无论如何,用户都必须处理擦除(并因此堆污染)。即使在运行时可以保留类型信息,始终会在课程的生成之前编译的Dusty Classfiles,因此堆中的任何给定的ArrayList都没有附加类型信息,具有伴随的风险堆污染。

某些有用的成语将是不可压缩的。现有的通用代码Wi

......