请选择 进入手机版 | 继续访问电脑版
查看: 122|回复: 0

泛目录技术 Proposals-译注-defense

[复制链接]

132

主题

134

帖子

713

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
713
发表于 2022-10-26 00:59:04 | 显示全部楼层 |阅读模式
最近我对提案更感兴趣。我看了一点JEP,下周我要看PEP。

无意中在知乎看到了一篇为擦除辩护的文章,Glavo 说的

“...(同构泛型)这种方法有一个其他方法无法获得的强大优势,那就是:渐进式迁移兼容性。这是一种在不破坏现有源代码或二进制类文件的情况下兼容转换在“

不是很懂,所以从openjdk官网翻译了这篇文章。这篇文章很棒,值得一看。- 注释

背景故事:我们现在如何获得泛型(或者,我如何学会停止担心和爱类型擦除)

布莱恩·戈茨

2020 年 6 月

在我们讨论泛型的去向之前,让我们先谈谈它们在哪里以及它们是如何到达那里的。本文档将主要关注我们如何获得我们现在拥有的泛型,以及为什么,作为我们当前的泛型将如何影响我们正在尝试构建的“更好”的泛型奠定基础的一种手段。

尤其是类型擦除是 Java 在 2004 年添加泛型时做出的明智而务实的选择,而我们使用擦除来实现翻译的原因至今仍然适用。

擦除

向任何开发人员询问有关 Java 泛型的问题,您可能会听到他抱怨擦除。因为擦除可能是 Java 中被最广泛误解的概念。

擦除不是特定于 java 的,也不是特定于泛型的。它通常是将代码转换为较低级别(例如从 java 到字节码,从 c 到源代码)的必要工具。这是因为当我们从高级语言到中间表示再到本机代码再到硬件堆栈时,较低级别提供的类型抽象几乎总是比较高级别提供的更简单和更弱——这是正确的(因为我们不想将虚拟调度的语义复制到 X86 指令集中,也不想在寄存器中模拟 Java 的原始类型集)。理想情况下,擦除是一种在对高级类型进行彻底类型检查后将高级类型表示映射到低级、不太丰富的类型的技术,并且是编译器每天都在做的事情。

例如,Java 字节码集包含在堆栈和局部变量集(iload、istore)之间移动整数值的指令,以及对 int 进行算术运算的指令(iadd、imul 等)。浮点数(fload、fstore、fmul 等)、长整数(lload、lstore、lmul)、双精度数(dload、dstore、dmul)和对象引用(aload、astore)也有类似的指令。) 但是对于字节、短字符、字符或布尔值没有这样的指令——因为编译器会将这些类型擦除为 int 并使用 int 移动和算术指令。这是字节码指令集设计中一个实际的设计折衷;它降低了指令集的复杂性,从而提高了运行时效率。Java 语言的许多其他特性(例如,检查异常、方法重载、枚举、明确赋值分析、

同构翻译和异构翻译

在具有参数多态性的语言中翻译泛型类型有两种常见的方式——同构翻译和异构翻译。在同构翻译中,泛型类 Foo<T> 被翻译成单个工件(可以理解为编译结果单元注解),例如 Foo.class(泛型方法也是如此)。在异构翻译中,泛型类型或方法(Foo<String>、Foo<Integer>)的每个实例都被视为一个单独的实体并产生一个单独的工件。例如,C++ 使用异构翻译:不同的模板实例具有完全不同的类型,具有不同的语义和不同的生成代码。向量和向量类型是不同的类型。一方面,这对于类型安全(每个实例化可以在扩展后单独进行类型检查)和生成代码的质量(因为每个实例化都可以单独优化)非常有用。另一方面,这意味着更大的代码占用空间(因为向量和向量有不同的代码),我们不能谈论“一些向量”(就像 Java 对通配符所做的那样),因为每个实例化都是完全不相关的类型。



作为可能的空间成本的一个极端演示,Scala 有一个 @specialized 注释,当应用于类型变量时,会导致编译器为所有原始类型发出专门的版本。这听起来很酷,但会导致生成的类激增,其中类中私有类型变量的数量是多少,因此可以从几行代码轻松生成一个 100MB 的 JAR。

在同质翻译和异质翻译之间进行选择涉及语言设计者一直在进行的各种权衡。异构翻译以更大的静态和动态占用空间以及更少的运行时共享为代价提供了更多的类型特异性——所有这些都对性能产生了影响。同构翻译更容易抽象出参数化类型族,例如 Java 的通配符或 C# 的声明站点变体(这两者在 C++ 中都缺乏,向量和向量之间没有任何共同点)。

Java类型擦除

Java 使用同构翻译来翻译泛型。泛型是在编译时进行类型检查的,但是在生成字节码时,像 List<String> 这样的泛型类型会被擦除到 List 中,而像 <T extends Object> 这样的类型变量会被绑定在其中,同时也会被擦除擦除(在这种情况下为对象)。

例如:

<p><pre>    <code class="prism language-java"><span class="token keyword">class</span> <span class="token class-name">Box</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">T</span><span class="token punctuation">></span></span> <span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token class-name">T</span> t<span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token class-name">Box</span><span class="token punctuation">(</span><span class="token class-name">T</span> t<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>t <span class="token operator">=</span> t<span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token class-name">Box</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">T</span><span class="token punctuation">></span></span> <span class="token function">copy</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Box</span><span class="token generics"><span class="token punctuation"><</span><span class="token punctuation">></span></span><span class="token punctuation">(</span>t<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token class-name">T</span> <span class="token function">t</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> t<span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></p>
javac 编译器生成一个类文件 Box.class,它用作 Box 的所有实例化的实现——包括通配符 (Box) 和原始类型 (Box)。字段、方法和超类描述符被擦除;类型变量被擦除到它们的边界,泛型类型被擦除到头部(例如,List&lt;String&gt; 到 List),如下所示:

<p><pre>    <code class="prism language-java"><span class="token keyword">class</span> <span class="token class-name">Box</span> <span class="token punctuation">{</span>
    <span class="token keyword">private</span> <span class="token class-name">Object</span> t<span class="token punctuation">;</span>
    <span class="token keyword">public</span> <span class="token class-name">Box</span><span class="token punctuation">(</span><span class="token class-name">Object</span> t<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>t <span class="token operator">=</span> t<span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token class-name">Box</span> <span class="token function">copy</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Box</span><span class="token punctuation">(</span>t<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token keyword">public</span> <span class="token class-name">Object</span> <span class="token function">t</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> t<span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></p>
泛型签名保存在 Signature 属性中,以便编译器在读取类文件时可以看到泛型签名,但 JVM 在链接时只使用擦除的描述符。这种转换方案意味着在类文件级别泛目录技术,Box&lt;T&gt; 的布局和 API 都被删除。在使用时,会发生同样的事情:对 Box&lt;String&gt; 的引用被删除为 Box,在使用时插入到 String 的合成转换。

为什么?有什么选择?

正是在这一点上,很容易生气并宣称这些显然是愚蠢或懒惰的选择,或者擦除是一种肮脏的做法。毕竟,为什么编译器会丢弃完美的类型信息?

为了更好地理解这个问题,我们还应该问:如果我们将这类信息具体化,我们想用它做什么,与它相关的成本是多少?我们可以想象几种使用具体类型参数信息的不同方式:

虽然这三种可能性并不相互排斥,但它们(反射、专业化和类型检查)有助于实现不同的目标(程序员的便利性、性能和安全性)——并且具有不同的含义和成本。虽然说“我们想要具体化”很容易,但如果我们再深入一点,我们会看到其中哪些是最重要的,它们的相对成本和收益差异很大。

要了解擦除在这里是多么明智和务实,我们还必须了解当时的目标、优先事项和约束以及替代方案。

目标:逐步迁移兼容性

Java 泛型采用了一个雄心勃勃的要求:

必须能够以二进制和源代码兼容的方式将现有的非泛型类演变为泛型。

这意味着现有的客户端和子类(例如 ArrayList)可以继续重新编译,而无需更改泛化的 ArrayList&lt;T&gt;,并且现有的类文件将继续链接到泛化的 ArrayList&lt;T&gt; T&gt; 方法。支持这一点意味着泛化类的客户端和子类可以选择现在泛化、稍后泛化或从不泛化,并且可以独立于其他客户端或子类的维护者选择做什么。

如果没有这个要求,泛化一个类将需要一个“标志日”,在此期间,如果不修改,所有客户端和子类都必须至少重新编译一次——完成。对于像 ArrayList 这样的核心类,它基本上要求世界上所有 Java 代码一次重新编译(或永久降级以节省 Java 1.4),我们需要一个允许核心平台类(和流行的第三方库)的通用类型系统) 被泛化,而不需要客户知道他们的泛型。(更糟糕的是,这不会是一个标志日,而是很多,因为世界上所有的代码都不会在一个原子事务中被概括。)

表达这种需求的另一种方式是删除所有可以被泛化的代码,或者让开发人员在泛型之间进行选择并保存他们在现有代码中已经完成的实现,这是不可接受的。通过使泛​​化成为兼容操作,可以保存此代码的实现而不是使其无效。

对“国旗日”的厌恶来自于 Java 设计的一个重要方面:**Java 是单独编译和动态链接的。** 单独编译是指将每个源文件编译成一个或多个类文件,而不是将一组源文件编译成单个工件。动态链接是指类之间的引用在运行时根据符号信息进行链接;如果类C调用D中的方法void m(int x),那么在C的类文件中我们会记录被调用的方法的名称和描述符((I)V),在链接时,我们会在D中查找方法使用此名称和描述符,如果找到匹配项,将链接呼叫站点。



这听起来可能需要做很多工作,但独立编译和动态链接是 Java 的最大优势之一——C 可以针对一个版本的 D 进行编译,并在类路径上使用不同版本的 D 运行(只要你不这样做)在 D 中执行任何二进制不兼容的更改)。

对动态链接的一般承诺允许我们简单地将新的 JAR 放在类路径上以更新到新版本的依赖项,而无需重新编译任何东西。我们经常这样做,以至于我们甚至都不关心它——但如果该方法停止,它确实会引起注意。

当泛型引入 Java 时,世界上已经有大量的 Java 代码,它们的类文件中充满了对 Java.util.ArrayList 等 API 的引用。如果我们不能兼容地概括这些 API,那么我们将不得不编写新的 API 来替换它们,更糟糕的是,旧 API 的所有客户端代码都将陷入无法支持的选择——要么永远使用 1.4,要么重写它们也可以使用新的 API(不仅包括应用程序代码,还包括应用程序所依赖的所有 3rd 方库)。这将使当时存在的几乎所有 Java 代码变得毫无价值。

C# 做出了相反的选择——更新了他们的 VM 实现,并使他们现有的库和所有依赖它的用户代码无效。他们当时可以这样做,因为世界上的 C# 代码相对较少;Java 当时没有这个选项。

但是,这种选择的一个结果是泛型类将同时具有泛型和非泛型用户或子类,这是预期的情况。这对软件开发过程非常有帮助,但在这种混合使用中,它对类型安全有潜在的影响。

堆污染

以这种方式擦除,并启用通用和非通用客户端之间的互操作性,会产生堆污染的可能性——存储在盒子中的运行时类型与预期的编译时类型不兼容。当客户端使用 Box&lt;String&gt; 时,每当将 T 分配给 String 时,都会插入一个强制转换,以便在数据从类型变量世界(Box 的实现)转换到具体类型世界时检测堆污染。在存在堆污染的情况下,这些强制转换可能会失败。

堆污染可能来自使用泛型类的非泛型代码,或者来自我们使用未经检查的强制转换或原始类型来伪造对错误泛型类型变量的引用。(当我们使用未经检查的强制转换或原始类型时,编译器会警告我们可能会导致堆污染。)

例如:

<p><pre>    <code class="prism language-java"><span class="token class-name">Box</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">String</span><span class="token punctuation">></span></span> bs <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Box</span><span class="token generics"><span class="token punctuation"><</span><span class="token punctuation">></span></span><span class="token punctuation">(</span><span class="token string">"hi!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token comment">// safe</span>
<span class="token class-name">Box</span><span class="token generics"><span class="token punctuation"><</span><span class="token operator">?</span><span class="token punctuation">></span></span> bq <span class="token operator">=</span> bs<span class="token punctuation">;</span>                      <span class="token comment">// safe, via subtyping</span>
<span class="token class-name">Box</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">Integer</span><span class="token punctuation">></span></span> bi <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token class-name">Box</span><span class="token generics"><span class="token punctuation"><</span><span class="token class-name">Integer</span><span class="token punctuation">></span></span><span class="token punctuation">)</span> bq<span class="token punctuation">;</span> <span class="token comment">// unchecked cast -- warning issued</span>
<span class="token class-name">Integer</span> i <span class="token operator">=</span> bi<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                <span class="token comment">// ClassCastException in synthetic cast to Integer</span>
</code></pre></p>
这段代码的问题是 Box&lt;? &gt; 未经检查的强制转换为 Box;我们必须让开发者相信指定的 Box 实际上是一个 box&lt;Integer&gt;。但堆污染并没有立即被发现。只有当我们尝试将框中的字符串用作整数时,我们才检测到出现问题。在我们的转换下,如果我们先将 box 转换为 box&lt;Integer&gt;,然后再将其用作 box&lt;String&gt;,然后再将其转换回 box&lt;String&gt;,则不会发生任何不好的事情(无论好坏)。

只要我们遵循以下规则,Java 实际上为泛型提供了非常强大的安全性:

如果程序在没有未经检查或原始警告的情况下编译,编译器插入的合成转换将永远不会失败。

换句话说泛目录技术,只有当我们与非泛型代码互操作或对编译器撒谎时,才会发生堆污染。在检测堆污染时,我们会得到一个简单明了的异常,告诉我们预期类型和实际类型。

上下文:JVM 实现和语言的生态系统

围绕泛型的设计选择还受到 JVM 实现和在 JVM 上运行的语言的生态系统结构的影响。虽然“Java”对于大多数开发人员来说是一个整体实体,但实际上 Java 语言和 Java 虚拟机 (JVM) 是独立的实体,每个实体都有自己的规范。Java 编译器为 JVM 生成类文件(其格式和语义在 Java 虚拟机规范中指定),但 JVM 将愉快地运行任何有效的类文件,而不管它最初来自什么源语言。据统计,以JVM为编译目标的语言有200多种,其中一些与Java语言有很多共同点(如Scala、Kotlin),另一些则大不相同(如JRuby、Jython、贾斯凯尔)

JVM 作为编译目标如此成功的原因之一,即使对于与 Java 完全不同的语言,也是因为它提供了一个相当抽象的计算模型,影响有限。语言与虚拟机之间的抽象层,不仅有助于激发运行在 JVM 上的其他语言的生态系统,也有助于激发 JVM 独立实现的生态系统。尽管今天的市场已经基本整合,但是当泛型添加到 Java 中时,有十几个商业上可行的 JVM 实现。将泛型具体化意味着我们不仅需要增强语言以支持泛型,还需要增强 JVM。

虽然当时在技术上可以为 JVM 添加泛型支持,但这不仅是一项重大的工程投资,需要许多实现者之间的大量协调和协议,JVM 上的语言生态系统也可能对具体化的泛型感兴趣。意见。例如,如果具体化的解释包括运行时类型检查,Scala(以及它的声明站点差异,例如逆变和协变)是否愿意让 JVM 强制执行 Java 的(不变的)通用子类型规则?

擦除是最务实的妥协

总而言之,这些约束(技术和生态系统)成为采用同构翻译策略的强大动力,该策略在编译时删除了泛型类型信息。总之,推动我们做出决定的因素包括:

显然,成本和风险将是巨大的;有什么好处?早些时候,我们列出了具体化的三个可能的好处:反射、布局专业化和运行时类型检查。上述论点基本上排除了我们可能进行运行时类型检查的可能性(运行时成本、不确定性风险、生态系统风险和已擦除实例的存在)。

当然,如果能够询问 List 它的元素类型是什么,那就太好了(也许它可以回答,但可能不会)——这显然是有好处的。只是成本和收益相差几个数量级。(选择泛型策略的另一个代价是原语不能用作类型参数;我们必须使用 List&lt;Integer&gt; 而不是 List&lt;Int&gt;。)

擦除是“肮脏的黑客”的普遍误解通常源于对替代品的真实成本缺乏认识,包括工程工作量、上市时间、交付风险、性能、生态系统影响,我们必须考虑已编写的大量 Java 代码以及在 JVM 上运行的 JVM 实现和语言的多样化生态系统。

豪侠泛目录站群程序,专业泛目录,站群,二级目录,泛站群程序!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表