Skip to content
欢迎扫码关注公众号

JEP 488: Primitive Types in Patterns, instanceof, and switch (Second Preview) | 模式、instanceof 和 switch 中的基本类型(第二次预览)

摘要

通过允许在所有模式上下文中使用原始类型来增强模式匹配,并扩展 instanceofswitch 以支持所有原始类型。这是 预览语言特性

历史

此特性最初由 JEP 455 提出,并作为预览特性在 JDK 23 中交付。我们在此提议第二次预览该特性,不做更改。

目标

  • 通过对所有类型(无论是原始类型还是引用类型)启用类型模式,实现统一的数据探索。

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

  • 允许模式匹配在嵌套和顶级模式上下文中使用原始类型。

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

  • 继承 Java 5 (枚举 switch) 和 Java 7 (字符串 switch) 中对 switch 的改进,允许 switch 处理任何原始类型的值。

非目标

  • 添加新的转换到 Java 语言不是目标之一。

动机

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

switch 的模式匹配不支持原始类型模式

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

使用 switch 中对原始类型模式的支持,我们可以改进 switch 表达式

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

通过将 default 子句变为带有暴露匹配值的原始类型模式的 case 子句:

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

支持原始类型模式还将允许守卫检查匹配的值:

java
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 数据:

java
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

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

不幸的是,如果我们希望使用记录模式分解 JsonNumber,Java 编译器不会如此配合。由于 JsonNumber 是用 double 组件声明的,我们必须针对 double 分解 JsonNumber 并手动转换为 int

java
if (json instanceof JsonObject(var map)
    && map.get("name") instanceof JsonString(String n)
    && map.get("age")  instanceof JsonNumber(double a)) {
    int age = (int)a;  // 不可避免(并且可能有损!)的类型转换
}

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

这种限制的原因在于缩小可能是有损的:运行时 double 组件的值对于 int 变量来说可能太大或精度太高。然而,模式匹配的一个关键好处是它能自动拒绝非法值,只需简单地不进行匹配。如果 JsonNumberdouble 组件太大或太精确以至于无法安全缩小到 int,那么 instanceof JsonNumber(int age) 只需返回 false,程序将在另一个分支中处理较大的 double 组件。

使用原始类型模式的支持,我们可以解除此限制。模式匹配可以在顶级以及嵌套在记录模式内部时保护将值缩小到原始类型的可能有损的转换。由于任何 double 都可以转换为 int,原始类型模式 int a 将适用于 JsonNumber 类型 double 的对应组件。只有当 double 组件可以无信息损失地转换为 int 时,instanceof 才会匹配模式并执行 if 分支,局部变量 a 将在范围内:

java
if (json instanceof JsonObject(var map)
    && map.get("name") instanceof JsonString(String n)
    && map.get("age")  instanceof JsonNumber(int a)) {
        ... n ...
        ... a ...
}

这将使得嵌套的原始类型模式能够像嵌套的引用类型模式一样顺畅工作。

instanceof 模式匹配不支持原始类型模式

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

原始类型模式在 instanceof 中就像在 switch 语句中一样有用。instanceof 的目的,广义上讲,是为了测试一个值是否可以安全地转换为给定类型;这就是为什么我们总是看到 instanceof 和类型转换操作紧密相连。由于将原始值从一种类型转换为另一种类型时可能出现的信息丢失,这种测试对于原始类型来说至关重要。

例如,即使可能有信息丢失,int 值到 float 的转换也可以通过赋值语句自动执行,并且开发者不会收到任何警告:

java
int getPopulation() {...}
float pop = getPopulation();  // 潜在的信息丢失是静默的

同时,将 int 值转换为 byte 需要显式转换,但由于转换可能是损失性的,因此必须先进行繁琐的范围检查:

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

如果 instanceof 支持原始类型模式,则可以取代 Java 语言内置的损失性转换,并避免开发人员近三十年来手工编写的细致入微的范围检查。换句话说,instanceof 不仅可以检查类型,还可以检查值。上述两个例子可以重写为:

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

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

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

instanceofswitch 中使用原始类型

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

java
if (i instanceof byte) {  // i的值适合byte
    ... (byte)i ...       // 需要传统的类型转换
}

这一改进使 instanceof Tinstanceof T t 的语义重新对齐,否则允许在一个上下文中使用原始类型而在另一个上下文中不允许就会破坏这种对齐。

最后,解除 switch 只能处理 byteshortcharint 值而不能处理 booleanfloatdoublelong 值的限制也将是有帮助的。

基于 boolean 值的切换将是三元条件运算符 (?:) 的一种有用的替代方式,因为 boolean 开关不仅能包含表达式也能包含语句。例如,下面的代码使用 boolean 开关在 false 情况下执行一些日志记录:

java
startProcessing(OrderStatus.NEW, switch (user.isLoggedIn()) {
    case true  -> user.id();
    case false -> { log("未识别的用户"); yield -1; }
});

基于 long 值的切换将允许 case 标签成为 long 常量,从而无需用单独的 if 语句处理非常大的常量:

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

描述

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

java
v instanceof JsonNumber(double a)

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

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

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

  3. 进一步增强instanceof结构,使其在用于类型测试而非模式匹配时,能够针对所有类型进行测试,而不仅仅是引用类型。这将扩展instanceof当前在引用类型上预条件安全转换的角色,应用于所有类型。

    更广泛地说,这意味着instanceof可以保障所有的转换,不论是匹配候选者的类型被测试(例如,x instanceof inty instanceof String)还是其值被匹配(例如,x instanceof int iy instanceof String s)。

  4. 进一步增强switch结构,使其不仅限于处理部分基本原始类型,而是适用于所有原始类型。

我们通过修改少量管理原始类型使用的 Java 语言规则,并确定一种类型到另一种类型的转换何时是安全的来实现这些变化——这涉及到要转换的值以及转换的源和目标类型的知识。

转换的安全性

如果转换不会导致信息丢失,则认为该转换是精确的。转换是否精确取决于所涉及的类型对以及输入值:

  • 对于某些类型对,在编译时就知道从第一种类型转换为第二种类型对于任何值都不会丢失信息。这种转换被称为无条件精确。对于无条件精确的转换,在运行时不需要任何操作。示例包括byteintintlongStringObject

  • 对于其他类型对,需要运行时测试来检查值能否从第一种类型转换为第二种类型而不丢失信息,或者如果执行强制转换的话,不会抛出异常。如果转换不会导致信息丢失或异常,则转换是精确的;否则,转换不精确。可能精确的转换示例包括longintintfloat,其中通过数值相等 (==) 或表示等价分别在运行时检测精度损失。从ObjectString的转换也需要运行时测试,转换是否精确取决于输入值动态上是否为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),即左操作数的类型必须是引用类型,并且右操作数必须指定一个引用类型。类型测试运算符变为:

java
InstanceofExpression:
    RelationalExpression instanceof Type
    ...

在运行时,我们通过精确转换扩展了 instanceof 对原始类型的支持:如果左侧的值可以通过精确转换转换为右侧的类型,则将其值转换为该类型是安全的,instanceof 返回 true

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

java
byte b = 42;
b instanceof int;         // true (无条件精确)

int i = 42;
i instanceof byte;        // true (精确)

int i = 1000;
i instanceof byte;        // false (不精确)

int i = 16_777_217;       // 2^24 + 1
i instanceof float;       // false (不精确)
i instanceof double;      // true (无条件精确)
i instanceof Integer;     // true (无条件精确)
i instanceof Number;      // true (无条件精确)

float f = 1000.0f;
f instanceof byte;        // false
f instanceof int;         // true (精确)
f instanceof double;      // true (无条件精确)

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

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

Integer ii = 16_777_217;
ii instanceof float;      // false (不精确)
ii instanceof double;     // true (精确)

我们并没有向 Java 语言添加任何新的转换,也没有更改现有的转换,更没有改变现有上下文(如赋值)中的哪些转换是允许的。instanceof 是否适用于给定值和类型取决于在类型转换上下文中是否允许转换以及它是否是精确的。例如,如果 b 是一个 boolean 变量,那么 b instanceof char 永远不允许,因为从 booleanchar 没有类型转换。

instanceofswitch 中的原始类型模式

类型模式合并了一个类型测试与一个条件转换。这避免了在类型测试成功时需要显式转换的需求,而未转换的值可以在类型测试失败时在不同的分支中处理。当 instanceof 类型测试运算符只支持引用类型时,只有引用类型模式才被允许用于 instanceofswitch;现在 instanceof 类型测试运算符支持原始类型,自然地允许在 instanceofswitch 中使用原始类型模式。

为此,我们取消了原始类型不能用于顶级类型模式的限制。因此,冗长且容易出错的代码

java
int i = 1000;
if (i instanceof byte) {    // false -- i 不能精确转换为 byte
    byte b = (byte)i;       // 可能存在数据丢失
    ... b ...
}

可以写成

java
if (i instanceof byte b) {
    ... b ...               // 没有信息丢失
}

因为 i instanceof byte b 意味着“测试 i instanceof byte,如果是这样,将 i 转换为 byte 并将该值绑定到 b”。

类型模式的语义由三个谓词定义:适用性、无条件性和匹配。我们按照以下方式解除对原始类型模式处理的限制:

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

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

  • 无条件性是指在编译时是否知道适用的模式将在运行时匹配匹配候选者的全部可能值。无条件模式不需要运行时检查。

    当我们将原始类型模式的应用范围扩大到更多类型时,我们必须指明它们在哪种类型上是无条件的。如果从 UT 的转换是无条件精确的,则原始类型模式对于类型 T 来说在类型为 U 的匹配候选者上是无条件的。这是因为无论输入值如何,无条件精确转换都是安全的。

  • 以前,如果不是 null 引用的值 v 匹配类型为 T 的类型模式,如果 v 可以转换为 T 而不抛出 ClassCastException。这种匹配的定义足以应对原始类型模式有限的角色。现在原始类型模式可以广泛使用,匹配被泛化为意味着一个值可以精确转换为 T,这涵盖了抛出 ClassCastException 以及潜在的信息丢失。

穷尽性

一个 switch 表达式,或者其 case 标签为模式的 switch 语句,要求是穷尽的:选择器表达式的所有可能值都必须在 switch 块中处理。如果 switch 包含一个无条件类型模式,则它是穷尽的;它也可能由于其他原因而穷尽,例如涵盖了密封类的所有可能允许的子类型。在某些情况下,即使存在不会被任何 case 匹配的可能运行时值,switch 仍可以被认为穷尽;在这种情况下,Java 编译器会插入一个合成的 default 子句来处理这些未预料到的输入。穷尽性在 模式:穷尽性、无条件性和剩余部分 中有更详细的介绍。

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

java
Byte b = ...
switch (b) {             // 穷尽的 switch
    case int p -> 0;
}

这种行为类似于记录模式的穷尽性处理方式。

正如 switch 使用模式穷尽性来确定 case 是否覆盖所有输入值一样,switch 使用支配关系来确定是否存在任何将不匹配任何输入值的 case

如果一种模式匹配另一种模式所匹配的所有值,则前者支配后者。例如,类型模式 Object o 支配类型模式 String s,因为所有能匹配 String s 的都会同样匹配 Object o。在 switch 中,如果一个没有保护的类型模式 Pcase 标签前置了一个类型模式为 Qcase 标签,且 P 支配 Q,则是非法的。支配的含义没有改变:如果类型模式 T t 对类型为 U 的匹配候选者是无条件的,则类型模式 T t 支配类型模式 U u

switch 中扩展的原始类型支持

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

如果选择器表达式的类型为 longfloatdoubleboolean,则 case 标签中使用的任何常量都必须与选择器表达式的类型或其对应的装箱类型相同。例如,如果选择器表达式的类型为 floatFloat,则任何 case 常量都必须是类型为 float 的浮点字面量(JLS §3.10.2)。这个限制是因为 case 常量和选择器表达式之间的不匹配可能导致有损转换,从而破坏程序员意图。下面的 switch 是合法的,但如果常量 0f 被错误地写成 0,则将是非法的。

java
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 标签。

java
float v = ...
switch (v) {
    case 1.0f -> ...
    case 0.999999999f -> ...    // 错误:重复的标签
    default -> ...
}

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

java
boolean v = ...
switch (v) {
    case true -> ...
    case false -> ...
    // 或者替代方案: case true, false -> ...
}

未来工作

在规范化 Java 语言的类型比较和模式匹配规则之后,我们可以考虑引入常量模式。目前,在 switch 中,常量只能作为 case 常量出现,例如这段代码中的 42

java
short s = ...
switch (s) {
    case 42 -> ...
    case int i -> ...
}

常量不能出现在记录模式中,这限制了模式匹配的实用性。例如,以下 switch 是不可能的:

java
record Box(short s) {}

Box b = ...
switch (b) {
    case Box(42) -> ...  // Box(42) 不是有效的记录模式
    case Box(int i) -> ...
}

得益于这里定义的适用性规则,常量可以被允许出现在记录模式中。在 switch 中,case Box(42) 将意味着 case Box(int i) when i == 42,因为 42 是类型为 int 的字面量。