跳到主要内容

JEP 455:模式、instanceof 和 switch 中的原始类型(预览)

QWen Max 中英对照 JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview)

总结

通过允许在所有模式上下文中使用基本类型模式来增强模式匹配,并扩展 instanceofswitch 以支持所有基本类型。这是一个 预览语言功能

目标

  • 通过允许所有类型(无论是基本类型还是引用类型)的类型模式,实现统一的数据探索。

  • 将类型模式与 instanceof 对齐,并将 instanceof 与安全转换对齐。

  • 允许模式匹配在嵌套和顶层上下文中使用基本类型模式。

  • 提供易于使用的构造,消除因不安全的类型转换而导致信息丢失的风险。

  • 在 Java 5(枚举 switch)和 Java 7(字符串 switch)中对 switch 进行增强之后,允许 switch 处理任何基本类型的值。

非目标

  • 目标并非是向 Java 语言添加新的转换类型。

动机

与原始类型相关的多种限制在使用模式匹配、instanceofswitch 时带来了不便。消除这些限制将使 Java 语言更加统一且更具表达力。

switch 的模式匹配不支持基本类型模式

第一个限制是,switch 的模式匹配(JEP 441)不支持基本类型模式,即指定基本类型的类型模式。仅支持指定引用类型的类型模式,例如 case Integer icase String s。(从 Java 21 开始,switch 也支持记录模式(JEP 440)。)

由于 switch 支持原始类型模式,我们可以改进 switch 表达式。

switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
default -> "unknown status: " + x.getStatus();
}

通过将 default 子句转换为带有原始类型模式的 case 子句,该模式会暴露匹配的值:

switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
case int i -> "unknown status: " + i;
}

支持原始类型模式还可以允许保护机制检查匹配的值:

switch (x.getYearlyFlights()) {
case 0 -> ...;
case 1 -> ...;
case 2 -> issueDiscount();
case int i when i >= 100 -> issueGoldCard();
case int i -> ... appropriate action when i > 2 && i < 100 ...
}

记录模式对原始类型的支持有限

另一个限制是,记录模式对基本类型的支持有限。记录模式通过将记录分解为其各个组成部分来简化数据处理。当一个组成部分是基本值时,记录模式必须精确匹配该值的类型。这对开发者来说很不方便,并且与 Java 语言其他部分中实用的自动转换功能不一致。

例如,假设我们希望处理通过这些记录类表示的 JSON 数据:

sealed interface JsonValue {
record JsonString(String s) implements JsonValue { }
record JsonNumber(double d) implements JsonValue { }
record JsonObject(Map<String, JsonValue> map) implements JsonValue { }
}

JSON 不区分整数和非整数,因此 JsonNumber 使用一个 double 类型的组件来表示数字,以实现最大的灵活性。然而,在创建 JsonNumber 记录时,我们不需要传递一个 double 类型的值;我们可以传递一个 int 类型的值,例如 30,Java 编译器会自动将 int 类型扩展为 double 类型:

var json = new JsonObject(Map.of("name", new JsonString("John"),
"age", new JsonNumber(30)));

然而,如果我们希望使用记录模式(record pattern)来分解 JsonNumber,Java 编译器并不会如此顺从。由于 JsonNumber 是用一个 double 类型的组件声明的,我们必须根据 double 来分解 JsonNumber,然后手动将其转换为 int 类型:

if (json instanceof JsonObject(var map)
&& map.get("name") instanceof JsonString(String n)
&& map.get("age") instanceof JsonNumber(double a)) {
int age = (int)a; // unavoidable (and potentially lossy!) cast
}

换句话说,原始类型模式可以嵌套在记录模式中,但它们是不变的:模式中的原始类型必须与记录组件的原始类型完全相同。不可能通过 instanceof JsonNumber(int age) 来分解 JsonNumber,并让编译器自动将 double 类型的组件缩小为 int 类型。

这种限制的原因在于,缩窄转换可能会导致数据丢失:运行时 double 类型的值可能过大,或者其精度超出了 int 变量能够容纳的范围。然而,模式匹配的一个关键优势在于,它可以通过简单地不匹配来自动拒绝非法值。如果 JsonNumberdouble 成分过大或过于精确,无法安全地缩窄为 int,那么 instanceof JsonNumber(int age) 可以直接返回 false,从而使程序在另一个分支中处理较大的 double 值。

引用类型模式的模式匹配已经可以这样工作了。例如:

record Box(Object o) {}
var b = new Box(...);

if (b instanceof Box(RedBall rb)) ...
else if (b instanceof Box(BlueBall bb)) ...
else ....

此处,Box 的组件被声明为 Object 类型,但可以使用 instanceof 尝试将 BoxRedBall 组件或 BlueBall 组件进行匹配。记录模式 Box(RedBall rb) 仅在运行时 b 是一个 Box 且其 o 组件可以缩小为 RedBall 时匹配;类似地,Box(BlueBall bb) 仅在其 o 组件可以缩小为 BlueBall 时匹配。

在记录模式中,原始类型模式应与引用类型模式一样顺畅地工作,即使对应的记录组件是 int 以外的数值原始类型,也应该允许使用 JsonNumber(int age)。这将消除在匹配模式后进行冗长且可能有损的类型转换的需要。

instanceof 的模式匹配不支持基本类型

另一个限制是,instanceof 的模式匹配(JEP 394)不支持基本类型模式。仅支持指定引用类型的类型模式。(从 Java 21 开始,instanceof 也支持记录模式。)

原始类型模式在 instanceof 中的用途与在 switch 中一样重要。instanceof 的作用大致来说,是测试一个值是否可以安全地转换为给定的类型;这就是为什么我们总是看到 instanceof 和强制类型转换操作紧密相连的原因。这种测试对于原始类型来说至关重要,因为在将原始类型的值从一种类型转换为另一种类型时,可能会发生信息丢失。

例如,将一个 int 值转换为 float 值即使可能会有精度损失,赋值语句也会自动执行这种转换 —— 而开发者却不会收到任何警告:

int getPopulation() {...}
float pop = getPopulation(); // silent potential loss of information

同时,将 int 值转换为 byte 需要使用显式强制转换,但由于这种转换可能会导致数据丢失,因此必须先进行繁琐的范围检查:

if (i >= -128 && i <= 127) {
byte b = (byte)i;
... b ...
}

instanceof 中的原始类型模式将会包含 Java 语言中内置的有损转换,并避免开发者近三十年来手动编写的繁琐范围检查。换句话说,instanceof 不仅可以检查类型,还可以检查值。上面的两个例子可以重写如下:

if (getPopulation() instanceof float pop) {
... pop ...
}

if (i instanceof byte b) {
... b ...
}

instanceof 运算符结合了赋值语句的便利性和模式匹配的安全性。如果输入(getPopulation()i)可以安全地转换为基本类型模式中的类型,则模式匹配成功,并且转换结果会立即可用(popb)。但是,如果转换会导致信息丢失,则模式不匹配,程序应在不同的分支中处理无效输入。

instanceofswitch 中的原始类型

如果我们要解除围绕原始类型模式的限制,那么解除一个相关的限制将会很有帮助:当 instanceof 接受一个类型而不是模式时,它只接受引用类型,而不接受原始类型。当使用原始类型时,instanceof 会检查转换是否安全,但不会实际执行该转换:

if (i instanceof byte) {  // value of i fits in a byte
... (byte)i ... // traditional cast required
}

instanceof 的这一增强恢复了 instanceof Tinstanceof T t 之间的语义一致性,如果我们允许在一种上下文中使用基本类型,而不在另一种上下文中使用,则这种一致性将会丧失。

最后,如果能取消 switch 可以接受 byteshortcharint 类型的值,但不能接受 booleanfloatdoublelong 类型值的限制,那就很有帮助了。

对于三元条件运算符(?:)来说,使用 boolean 值进行切换将是一个有用的替代方案,因为 boolean 切换既可以包含语句,也可以包含表达式。例如,以下代码使用 boolean 切换来在值为 false 时执行一些日志记录操作:

startProcessing(OrderStatus.NEW, switch (user.isLoggedIn()) {
case true -> user.id();
case false -> { log("Unrecognized user"); yield -1; }
});

启用 long 值的切换将允许 case 标签作为 long 类型的常量,从而无需使用单独的 if 语句来处理非常大的常量:

long v = ...;
switch (v) {
case 1L -> ...;
case 2L -> ...;
case 10_000_000_000L -> ...;
case 20_000_000_000L -> ...;
case long x -> ... x ...;
}

描述

在 Java 21 中,原始类型模式只允许作为记录模式中的嵌套模式,例如:

v instanceof JsonNumber(double a)

为了通过模式匹配来更统一地支持对匹配候选 v 的数据探索,我们将:

  1. 扩展模式匹配,使原始类型模式能够适用于更广泛的匹配候选类型。这将允许诸如 v instanceof JsonNumber(int age) 这样的表达式。

  2. 增强 instanceofswitch 构造,以支持原始类型模式作为顶级模式。

  3. 进一步增强 instanceof 构造,以便在用于类型测试而不是模式匹配时,可以测试所有类型,而不仅仅是引用类型。这将扩展 instanceof 的当前作用,即作为对引用类型进行安全转换的前提条件,使其适用于所有类型。

    更广泛地说,这意味着 instanceof 可以保护所有转换,无论匹配候选类型是进行类型测试(例如,x instanceof int,或 y instanceof String)还是进行值匹配(例如,x instanceof int i,或 y instanceof String s)。

  4. 进一步增强 switch 构造,使其适用于所有原始类型,而不仅仅是 整数原始类型 的一个子集。

我们通过对 Java 语言中控制原始类型使用的少量规则进行更改来实现这些变化:

  • 移除 instanceofswitch 结构中对原始类型和原始类型模式的限制;

  • 扩展 switch 以处理所有原始类型字面量的常量情况;以及

  • 确定从一种类型转换为另一种类型何时是安全的,这涉及对要转换的值以及转换的源类型和目标类型的了解。

转换的安全性

如果信息没有丢失,则转换是精确的。转换是否精确取决于所涉及的类型对以及输入值:

  • 对于某些类型对,编译时即可确定从第一个类型转换为第二个类型时,对于任何值都保证不会丢失信息。这种转换被称为无条件精确转换。对于无条件精确转换,在运行时无需采取任何操作。例如,byte 转换为 intint 转换为 long 以及 String 转换为 Object

  • 对于其他类型对,则需要在运行时进行测试,以检查是否可以在不丢失信息的情况下将值从第一个类型转换为第二个类型,或者如果执行强制类型转换,是否会抛出异常。如果不会发生信息丢失或异常,则转换是精确的;否则,转换不精确。可能精确的转换示例包括 long 转换为 intint 转换为 float,其中精度损失分别通过数值相等性 (==) 或表示等价性 在运行时检测到。从 Object 转换为 String 同样需要运行时测试,转换是否精确取决于输入值在运行时是否动态为 String 类型。

简而言之,如果从一种基本数据类型转换为另一种时是无条件精确的,那么它应该是从一个整数类型拓宽到另一个整数类型、从一个浮点类型拓宽到另一个浮点类型、从 byteshortchar 转换为浮点类型,或者从 int 转换为 double。此外,装箱转换和拓宽引用转换也是无条件精确的。

下表表示允许在基本类型之间进行的转换。无条件精确转换用符号 ɛ 表示。符号 表示恒等转换,ω 表示拓宽原始转换,η 表示缩窄原始转换,ωη 表示拓宽和缩窄原始转换。符号 表示不允许转换。

转换至 →byteshortcharintlongfloatdoubleboolean
从 ↓
byteɛωηɛɛɛɛ
shortηηɛɛɛɛ
charηηɛɛɛɛ
intηηηɛωɛ
longηηηηωω
floatηηηηηɛ
doubleηηηηηη
boolean

将此表与其在 JLS §5.5 中的等价表进行比较,可以看出 JLS §5.5 中 ω 所允许的许多转换被“升级”为上述无条件精确的 ɛ

instanceof 作为安全类型转换的前提条件

使用 instanceof 进行的类型测试传统上仅限于引用类型。instanceof 的经典含义是一种前提条件检查,其询问:将这个值转换为此类型是否安全且有用?这个问题对于原始类型而言甚至比引用类型更加相关。对于引用类型,如果意外遗漏了检查,执行不安全的强制类型转换可能不会造成损害:会抛出 ClassCastException 异常,并且被错误转换的值将无法使用。相比之下,对于原始类型,由于没有方便的方法来检查安全性,执行不安全的强制类型转换可能会导致隐蔽的错误。它不会抛出异常,而是可能悄无声息地丢失信息,例如数值大小、符号或精度,从而使被错误转换的值流入程序的其余部分。

为了在 instanceof 类型测试操作符中启用基本类型,我们移除了以下限制(JLS §15.20.2):左操作数的类型必须是引用类型,并且右操作数必须指定引用类型。类型测试操作符变为:

InstanceofExpression:
RelationalExpression instanceof Type
...

在运行时,我们通过精确转换将 instanceof 扩展到原始类型:如果左侧的值可以通过精确转换转换为右侧的类型,那么将该值转换为该类型是安全的,并且 instanceof 会报告 true

以下是扩展的 instanceof 如何保护强制类型转换的一些示例。无条件精确转换无论输入值如何都返回 true;所有其他转换都需要运行时测试,其结果如下所示。

byte b = 42;
b instanceof int; // true (unconditionally exact)

int i = 42;
i instanceof byte; // true (exact)

int i = 1000;
i instanceof byte; // false (not exact)

int i = 16_777_217; // 2^24 + 1
i instanceof float; // false (not exact)
i instanceof double; // true (unconditionally exact)
i instanceof Integer; // true (unconditionally exact)
i instanceof Number; // true (unconditionally exact)

float f = 1000.0f;
f instanceof byte; // false
f instanceof int; // true (exact)
f instanceof double; // true (unconditionally exact)

double d = 1000.0d;
d instanceof byte; // false
d instanceof int; // true (exact)
d instanceof float; // true (exact)

Integer ii = 1000;
ii instanceof int; // true (exact)
ii instanceof float; // true (exact)
ii instanceof double; // true (exact)

Integer ii = 16_777_217;
ii instanceof float; // false (not exact)
ii instanceof double; // true (exact)

我们不会向 Java 语言添加任何新的转换,也不会更改现有的转换,更不会改变在诸如赋值之类的现有上下文中允许的转换。instanceof 是否适用于给定的值和类型,取决于在强制类型转换上下文中是否允许该转换以及它是否是精确的。例如,如果 b 是一个 boolean 变量,则绝不允许使用 b instanceof char,因为从 booleanchar 没有强制类型转换。

instanceofswitch 中的原始类型模式

类型模式将类型测试与条件转换相结合。这样,如果类型测试成功,则无需显式强制转换;而如果类型测试失败,未转换的值可以在另一个分支中处理。当 instanceof 类型测试操作符仅支持引用类型时,在 instanceofswitch 中只允许使用引用类型模式是很自然的;现在 instanceof 类型测试操作符支持原始类型了,那么在 instanceofswitch 中允许使用原始类型模式也是很自然的。

为了实现这一点,我们取消了原始类型不能用于顶层类型模式的限制。因此,繁琐且容易出错的代码:

int i = 1000;
if (i instanceof byte) { // false -- i cannot be converted exactly to byte
byte b = (byte)i; // potentially lossy
... b ...
}

可以写为

if (i instanceof byte b) {
... b ... // no loss of information
}

因为 i instanceof byte b 的意思是“测试 i 是否为 byte 类型,如果是,则将 i 转换为 byte 并将其值绑定到 b”。

类型模式的语义由三个谓词定义:适用性、无条件性和匹配性。我们对基本类型模式的处理限制如下放宽:

  • 适用性 是指某个模式在编译时是否合法。以前,对于原始类型模式的适用性要求输入表达式的类型与模式中的类型完全相同。例如,switch (... an int ...) { case double d: ... } 是不允许的,因为模式 double 对于 int 不适用。

    现在,如果类型为 U 的匹配候选值可以转换为类型 T 而不会产生未经检查的警告,则类型模式 T t 对该匹配候选值适用。由于 int 可以转换为 double,因此这个 switch 现在是合法的。

  • 无条件性 是指在编译时是否能够确定一个适用的模式将匹配所有可能的运行时值。无条件模式不需要运行时检查。

    随着我们扩展原始类型模式以适用于更多类型,我们必须明确它们在哪些类型上是无条件的。如果从 UT 的转换是无条件精确的,则类型为 T 的原始类型模式对于类型为 U 的匹配候选值是无条件的。这是因为无条件精确转换无论输入值如何都是安全的。

  • 以前,如果一个非 null 引用的值 v 能够在不抛出 ClassCastException 的情况下转换为类型 T,则认为它匹配类型为 T 的类型模式。当原始类型模式的作用有限时,这种匹配定义是足够的。现在,随着原始类型模式可以广泛使用,匹配被泛化为意味着值可以精确转换为 T,这既包括抛出 ClassCastException 的情况,也包括潜在的信息丢失。

完备性

一个 switch 表达式,或者其 case 标签为模式的 switch 语句,必须是详尽无遗的:选择器表达式的所有可能值都必须在 switch 块中得到处理。如果 switch 包含一个无条件的类型模式,则它是详尽无遗的;它也可能由于其他原因而变得详尽无遗,例如覆盖了密封类的所有可能允许的子类型。在某些情况下,即使存在无法被任何 case 匹配的可能运行时值,switch 仍可被视为详尽无遗;在这种情况下,Java 编译器会插入一个合成的 default 子句来处理这些未预料到的输入。详尽性在 Patterns: Exhaustiveness, Unconditionality, and Remainder 中有更详细的讨论。

随着原始类型模式的引入,我们为穷尽性判定添加了一条新规则:对于某个匹配候选为原始类型 P 的包装类型 Wswitch 语句,如果类型模式 T tP 上无条件精确,则 T t 能够穷尽 W。在这种情况下,null 将成为剩余部分。在以下示例中,匹配候选是原始类型 byte 的包装类型,并且从 byteint 的转换是无条件精确的。因此,以下 switch 是穷尽的:

Byte b = ...
switch (b) { // exhaustive switch
case int p -> 0;
}

此行为类似于记录模式的详尽性处理。

正如 switch 使用模式穷举来确定 case 是否覆盖所有输入值一样,switch 还使用支配性来确定是否存在无法匹配任何输入值的 case。

如果一个模式匹配另一个模式所能匹配的所有值,那么我们称该模式“主导(dominates)”另一个模式。例如,类型模式 Object o 主导类型模式 String s,因为所有能匹配 String s 的值也都能匹配 Object o。在 switch 语句中,如果类型模式 P 主导类型模式 Q,那么带有无保护类型模式 Pcase 标签不能出现在带有类型模式 Qcase 标签之前。主导的含义保持不变:如果类型模式 T t 在匹配类型为 U 的候选值时是无条件的,则类型模式 T t 主导类型模式 U u

switch 中扩展原始类型支持

我们增强了 switch 结构,以允许选择器表达式为 longfloatdoubleboolean 类型,以及相应的包装类型。

如果选择器表达式的类型为 longfloatdoubleboolean,则 case 标签中使用的任何常量必须与选择器表达式具有相同的类型,或者是其对应的包装类型。例如,如果选择器表达式的类型是 floatFloat,那么任何 case 常量都必须是类型为 float 的浮点数字面量(JLS §3.10.2)。这一限制是必要的,因为 case 常量与选择器表达式之间的不匹配可能会引入有损转换,从而违背程序员的意图。以下 switch 语句是合法的,但如果常量 0f 被意外写成 0,则是非法的。

float v = ...
switch (v) {
case 0f -> 5f;
case float x when x == 1f -> 6f + x;
case float x -> 7f + x;
}

case 标签中浮点数字面量的语义是根据编译时和运行时的表示等价性定义的。使用两个表示等价的浮点数字面量是编译时错误。例如,以下 switch 是非法的,因为字面量 0.999999999f 被向上舍入为 1.0f,从而创建了重复的 case 标签。

float v = ...
switch (v) {
case 1.0f -> ...
case 0.999999999f -> ... // error: duplicate label
default -> ...
}

由于 boolean 类型只有两个不同的值,因此列出 truefalse 两种情况的 switch 被认为是穷尽的。以下 switch 是合法的,但如果存在 default 子句,则它是非法的。

boolean v = ...
switch (v) {
case true -> ...
case false -> ...
// Alternatively: case true, false -> ...
}