Valhalla (7): 解读 JEP 402 Unify the Basic Primitives with Objects

解读 JEP 402: Unify the Basic Primitives with Objects (Preview),统一基本类型和对象。

以直译为主,部分内容会意译,一些地方会增加自己的解读。一些概念解释见 深入理解 Valhalla (0): 序言

系列文章(未完待续)

摘要 Summary

通过将基本值设计为原始类(见 JEP 401)的实例,统一基本类型(intdouble 等)和对象,继续用包装类的声明作为原始类的声明。作为改动的结果,Java 中所有的值都是对象了。该提案将会是一项预览特性。

目标 Goals

  • 将 8 个包装类(java.lang.Integerjava.lang.Double 等)迁移为引用优先(Reference-favoring)的原始类。
  • 在 Java 语言中,将基本值视为迁移后的包装类的实例。同时基本类型关键字(intdouble 等)将作为对应原始值类型的别名,新的类型支持方法调用、原始引用转换、数组协变。
  • 在 Java 虚拟机中,将基本数组类型视为对应原始对象的数组类型。
  • 在核心反射 API 中,修改 8 个表示基本类型的 Class 对象(int.classdouble.class 等)的行为,以便符合它们的类型定义。

这将是一项非常重大的改动,Java 真正变成了一切皆对象,以下代码都将变得合法:

1
2
3
4
5
1.equals(1); // 支持方法调用
int.ref one = 1; // 原始引用转换
Object[] array = new int[]{1, 2, 3}; // 数组协变
// int == Integer.val 等价
// int.ref == Integer 等价

非目标 Non-Goals

  • 原始对象和原始类的功能在 JEP 401中介绍,而该 JEP 只考虑如何在 8 个基本类型上应用这些特性。
  • 该 JEP 不涉及原始值类型(intdouble 等)和 Java 泛型的交互。单独的 JEP 将满足原始值类型作为类型参数的需求,并最终优化这些参数化的性能。
  • 该 JEP 不会提出任何新的数字基本类型,也没有为 Java 的一元和二元操作符提出任何新功能。·

上述第三点,目前在 Java 中不支持无符号数字,也不支持 128 位整数,但可以预期,未来可以基于原始对象的方式来支持。

动机 Motivation

Java 是面向对象的语言,但基本值(布尔、整数和浮点数)都不是对象。在创建语言时,这是一个明智的设计选择,因为每个对象都需要额外的内存开销和间接寻址。但也意味着基本值不支持对象的一些有用的特性,例如实例方法、子类型和后来的泛型。

作为一种解决方案,原来的标准库提供了包装类,每个类都存储了一个基本值,并作为对象呈现。Java 5 引入了隐式的装箱/拆箱,可以根据程序要求把基本类型的值转换成包装类的实例,反之亦然。

但包装类这样的解决方案是不完美的,它并没有完全屏蔽转换的影响(例如同一个值装箱两次,得到的对象可能并不 ==)。更重要的是,在许多应用程序中,将基本值包装在对象中有明显的运行时成本,开发人员必须权衡这些成本和更强大的表达能力。

JEP 401 中介绍了原始对象(Primitive Objects)的特性,将 Identity-free 的值设计为原始对象,能够消除了大部分的额外开销。作为结果,现在可以将基本值在所有的上下文中都当作第一等的对象。最后,我们可以宣称所有的值都是对象。

每个原始对象需要一个原始类,那么 int 的值应该是属于哪个类?大部分现有的代码,都会假设基本值作为对象,应该属于包装类。由于不再需要对基本值进行包装,最小的改动是将包装类改造为原始类,例如 int 值作为 java.lang.Integer 的实例,boolean 值作为 java.lang.Boolean 的实例等。

通过将基本类型修改为原始类,我们可以给它们增加实例方法,并且集成到子类型图中(class subtyping graph)。未来的 JEP 将追求原始值类型和 Java 泛型的交互。

描述 Description

以下描述是预览中的特性,需要在编译和运行时增加 --enable-preview 参数。

基本原始类 Basic Primitive Classes

8 个基本原始类如下

  • java.lang.Boolean
  • java.lang.Character
  • java.lang.Byte
  • java.lang.Short
  • java.lang.Integer
  • java.lang.Long
  • java.lang.Float
  • java.lang.Double

编译器和引导类加载器会通过特殊的方式定位类文件,如果开启了预览特性,会定位到修改之后的类。

修改之后的原始类,它们是引用优先(Reference-favoring)的,意味着 IntegerDouble 等类型名继续指它们的引用类型(原始引用类型)。

这些类的公开构造函数,在 JDK 16 中已经被 JEP 390 标记为计划移除(forRemoval)。涉及到 Identity 和原始类构造函数编译方式不同,可能会产生微妙的二进制兼容性问题。为了避免该问题,修改后的类的构造函数是私有的。

Java 语言模型 Java Language Model

8 个基本类型关键字(booleancharbyteshortintlongfloatdouble)将作为基本原始类和对应原始值类型的别名。.ref 语法被用来表示对应的引用类型。

因为它们是别名,因此每个原始类、原始值类型、原始引用类型都有两种方法表示,见如下表格:

原始类 Primitive Class 值类型 Value Type 引用类型 Reference Type
boolean / Boolean boolean / Boolean.val boolean.ref / Boolean
char / Character char / Character.val char.ref / Character
byte / Byte byte / Byte.val byte.ref / Byte
short / Short short / Short.val short.ref / Short
int / Integer int / Integer.val int.ref / Integer
long / Long long / Long.val long.ref / Long
float / Float float / Float.val float.ref / Float
double / Double double / Double.val double.ref / Double

代码风格上的问题,我们建议使用小写字母、基于关键字的方式。

原始类声明的限制在基本原始类上会有特例:基本原始类可以递归声明自身原始值类型的字段(例如 int 类可以包含 int 类型的字段)。

Java 支持基本类型之间的转换(例如 intdouble),这些行为不会发生变化。为清晰期间,我们现在称之为扩展数字转换(Widening Numeric Conversions)和收缩数字转换(Narrowing Numeric Conversions)。引用类型(例如 int.refdouble.ref)之间是没有类似的转换的。

装箱/拆箱将会被原始类的原始引用转换(Primitive Reference Conversions)和原始值转换(Primitive Value Conversions)替代。支持的类型是一样的,但运行时的行为会更加高效。

Java 支持一系列的一元和二元操作符来操作基本值(例如 23 * 12!true),这些操作的规则和行为不会变化。

因为基本值是对象,它们类定义里面也可以定义实例方法。23.compareTo(42) 这样的代码现在是合法的。(TODO:这是否会导致任何语法分析的问题?equalscompareTo 这样的行为是否有意义?)

和其他原始值类型一样,基本原始值类型的数组是协变的:int[] 可以视为 int.ref[]Number[] 等。

编译期和运行时 Compilation and Run Time

在 JVM 中,基本类型和原始类类型是不同的:类型 D 表示 64 位浮点数,占用栈上两个 slot,支持一套专有的操作码 (dloaddstoredadddcmpg 等)。类型 Qjava/lang/Double; 表示 Double 的原始对象,占用栈上一个 slot,支持对象的操作码 (aloadastoreinvokevirtual 等)。

Java 编译器有义务根据需求适配两种类型,通过调用方法例如 Double.valueOfDouble.doubleValue。编译后的字节码跟装箱/拆箱生成的字节码类似,但是运行时开销会大幅减少。

为了一致性,基本原始值类型出现在字段类型和方法签名的时候,永远编译成 JVM 中的基本类型(D 而不是 Qjava/lang/Double;),除了基本原始类本身的定义(例如 Double.valueOf 返回类型 QDouble;)。

编译器的适配,对于基本数组类型来说是不够的。例如通过 newarray 创建的类型为 [D 的数组,可以传递给期望参数为 [Ljava/lang/Double; 的方法。同时通过 anewarray 创建的类型为 [Qjava/lang/Double; 的数组,可以转换为 [D。为了支持上述行为,JVM 将 [D[Qjava/lang/Double; 视为兼容的类型。他们的值同时支持两套操作码(daloadaaloaddastoreaastore),无论数组是如何创建的。
(TODO:getClass().getComponentType() 将如何返回)

核心反射 Core Reflection

每一个基本原始类型,开发人员通常会遇到两个 Class 对象。以 Double 类为例,有:

  • double.class(或等价的 Double.val.class)对应 JVM 的描述符类型 DisPrimitive() 方法返回 true。当开启预览特性的时候,为了跟 Java 语言模型对齐,大多数其他行为类似于 java.lang.Double(例如 isPrimitiveClassgetMethodsgetSuperclass 等)。
  • Double.class(或等价的 double.ref.class)对应 JVM 的描述符类型 Ljava/lang/Double;isPrimitive() 返回 false。跟表示原始引用类型的标准 Class 对象的行为是类似的。

基本原始对象 getClass() 方法返回第二种 Class 对象(例如 Double.classInteger.class 等)。对所有的原始对象来说,通过值类型((23.0).getClass())或者引用类型(((Double)23.0).getClass())返回的都是一样的。这跟传统的装箱行为是兼容的。

(23.0).getClass() == Double.class

还存在第三个 Class 对象,对应 JVM 的描述符类型 Qjava/lang/Double;,但在实践中几乎不会使用,因为 Java 编译器几乎不会用该类型描述符名称。也没有类名表示这些对象。isPrimitive() 返回 false。跟表示原始值类型的标准 Class 对象的行为是类似的。

也就是说原始类 Person.class 的描述符是 QPoint;Person.ref.class 的描述符是 LPoint;
按照相同的逻辑来说 double.class 的描述符是 Qjava/lang/Double;double.ref.class 的描述符是 Ljava/lang/Double;
但 Java 编译器在遇到 double.classDouble.val.class 时为了兼容都会编译成 D,算是一种特殊的处理。

其他选择 Alternatives

语言可以保持不变(原始对象是一个有用的特性,但无需将基本值视为对象)。但是消灭基本类型和对象之间的隔阂会非常有用,特别是当 Java 的泛型支持原始对象时。

新类可以作为基本原始类(例如 java.lang.int)引入,而将包装类作为遗留 API。 但是关于装箱行为的假设在某些代码中根深蒂固,而一组新的类会破坏这些程序。

JVM 可以遵循 Java 语言将其基本类型(ID 等)与其原始类类型(Qjava/lang/Integer;Qjava/lang/Double; 等)完全统一。但这将是一个代价高昂的改变,最终收益甚微。 例如,必须有一种方法来协调两个 slot 的 D 类型和一个 slot 的 Qjava/lang/Double; 类型,可能需要对类文件格式进行破坏性的版本更改。

风险和假设 Risks and Assumptions

删除包装类的构造函数,破坏了遗留 Java 程序的二进制兼容性。还有与迁移到原始类型相关的行为变化。JEP 390 以及一些预期的后续工作减轻了这些负担。但一些调用构造函数或者依赖装箱对象 Identity 的程序会发生错误。

由于新的基本原始类型将作为类类型,反射行为的变化可能导致某些程序出现问题。存在表示 Qjava/lang/Double; 类型的不同对象,很容易被忽略并可能让一些开发人员感到惊讶。

依赖 Dependencies

JEP 401 原始对象是必备的条件。
考虑到该功能,JEP 390 已经对原始类的候选者们可能产生的不兼容改动,向 javac 和 HotSpot 增加了警告。一些后续工作将在其他的 JEP 中进行。
我们期望修改 Java 中的泛型模型,使类型参数更加通用(可以被所有的类实例化,包括引用和值)。这会在单独的 JEP 中进行。

Valhalla (7): 解读 JEP 402 Unify the Basic Primitives with Objects

https://www.alphalxy.com/2021/11/jep-402/

Author

Xinyu Liu

Posted on

2021-11-06

Updated on

2022-05-07


Comments