JEP 488: 原始类型在模式、instanceof 和 switch 中的使用(第二次预览)
概述
通过允许在所有模式匹配上下文中使用基本类型,并扩展 instanceof 和 switch 以支持所有基本类型,来增强模式匹配。这是一个预览语言特性。
历史
此功能最初由 JEP 455 提出,并在 JDK 23 中作为预览功能交付。我们在此提议再次对其进行预览,不做任何更改。
目标
-
通过允许所有类型(无论是原始类型还是引用类型)的类型模式,实现统一的数据探索。
-
使类型模式与
instanceof保持一致,并使instanceof与安全类型转换保持一致。 -
允许模式匹配在嵌套和顶级模式上下文中使用原始类型。
-
提供易于使用的结构,以消除由于不安全的类型转换而丢失信息的风险。
-
在 Java 5(枚举
switch)和 Java 7(字符串switch)对switch的增强之后,允许switch处理任何原始类型的值。
非目标
- 不打算为 Java 语言添加新的转换类型。
动机
与基本类型相关的多种限制在使用模式匹配、instanceof 和 switch 时造成了不便。消除这些限制将使 Java 语言更加统一和更具表现力。
switch 的模式匹配不支持基本类型模式
随着对 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)));
不幸的是,如果我们希望用记录模式分解 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 变量来说精度太高。然而,模式匹配的一个关键优势是它能够自动拒绝非法值,通过简单地不匹配来实现。如果 JsonNumber 的 double 成员太大或精度太高而无法安全地收窄为 int,那么 instanceof JsonNumber(int age) 可以简单地返回 false,让程序在不同的分支中处理大 double 成员。
有了对基本类型模式的支持,我们可以解除这一限制。模式匹配可以保护值向基本类型的可能有损的窄化转换,这既可以在顶层进行,也可以在嵌入记录模式中时进行。由于任何 double 都可以转换为 int,因此基本类型模式 int a 可以应用于类型为 double 的 JsonNumber 的相应组件。如果并且只有当 double 组件可以无信息丢失地转换为 int 时,instanceof 才会匹配该模式,并且会执行 if 分支,同时局部变量 a 在作用域内:
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 会通过赋值语句自动执行,即使这可能会丢失数据——而开发者不会收到任何警告:
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)可以安全地转换为原始类型模式中的类型,则模式匹配,并且转换的结果立即可用(pop 或 b)。但是,如果转换会丢失信息,则模式不匹配,程序应在不同的分支中处理无效输入。
原始类型在 instanceof 和 switch 中
如果我们要解除对原始类型模式的限制,那么解除一个相关限制也会有所帮助:当 instanceof 接受一个类型而不是模式时,它只接受引用类型,而不接受原始类型。当接受原始类型时,instanceof 会检查转换是否安全,但实际上并不会执行转换:
if (i instanceof byte) { // value of i fits in a byte
... (byte)i ... // traditional cast required
}
此增强对 instanceof 的改进恢复了 instanceof T 和 instanceof T t 之间的语义一致性,如果我们允许在一个上下文中使用基本类型而在另一个上下文中不允许的话,这种一致性将会丢失。
最后,解除 switch 只能接受 byte、short、char 和 int 值而不能接受 boolean、float、double 或 long 值的限制将是有帮助的。
如果可以对 boolean 值进行 switch 操作,那它会成为三元条件运算符(?:)的一个有用的替代方案,因为 boolean 的 switch 可以包含语句和表达式。例如,下面的代码使用了一个 boolean 的 switch,在 false 时执行一些日志记录:
switch (someBoolean) {
case false:
System.out.println("The value is false");
// 可以在这里添加更多的语句
break;
case true:
System.out.println("The value is true");
// 也可以在这里添加更多的语句
break;
}
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,我们将:
-
扩展模式匹配,使原始类型模式适用于更广泛的匹配候选类型。这将允许使用诸如
v instanceof JsonNumber(int age)的表达式。 -
增强
instanceof和switch构造,以支持将原始类型模式作为顶级模式。 -
进一步增强
instanceof构造,使其在用于类型测试而不是模式匹配时,可以测试所有类型,而不仅仅是引用类型。这意味着instanceof当前作为引用类型安全转换的先决条件的角色将扩展到所有类型。更广泛地说,这意味着
instanceof可以保护所有转换,无论匹配候选者是在进行类型测试(例如,x instanceof int或y instanceof String)还是值匹配(例如,x instanceof int i或y instanceof String s)。 -
进一步增强
switch构造,使其与所有原始类型一起工作,而不仅仅是部分整数原始类型。
我们通过更改 Java 语言中管理基本类型使用的一些规则来实现这些更改,并通过表征从一种类型转换为另一种类型何时是安全的来实现这些更改 —— 这涉及对要转换的值以及转换的源类型和目标类型的了解。
转换的安全性
如果在转换过程中没有信息丢失,则该转换是精确的。转换是否精确取决于所涉及的类型对以及输入值:
-
对于某些类型对,在编译时已知从第一种类型转换为第二种类型可以保证不会丢失任何值的信息。这种转换被称为无条件精确。对于无条件精确的转换,在运行时不需要采取任何操作。例如
byte到int,int到long,以及String到Object。 -
对于其他类型对,需要在运行时进行测试,以检查该值是否可以从第一种类型转换为第二种类型而不会丢失信息,或者如果执行了类型转换,是否会抛出异常。如果不会发生信息丢失或异常,则转换是精确的;否则,转换不是精确的。可能精确的转换示例包括
long到int和int到float,其中精度损失分别通过使用数值相等 (==) 或表示等价在运行时检测。从Object到String的转换也需要运行时测试,并且根据输入值是否动态地为String来判断转换是否精确。
简而言之,如果从一种整型类型拓宽到另一种整型类型,或者从一种浮点类型拓宽到另一种浮点类型,或者从 byte、short 或 char 拓宽到浮点类型,或者从 int 拓宽到 double,那么原始类型之间的转换是无条件精确的。此外,装箱转换和拓宽引用转换也是无条件精确的。
下表列出了允许的原始类型之间的转换。无条件精确转换用符号 ɛ 表示。符号 ≈ 表示恒等转换,ω 表示扩展原始类型转换,η 表示缩小原始类型转换,ωη 表示扩展和缩小原始类型转换。符号 — 表示不允许转换。
到 →
字节
short
char
int
long
float
double
boolean
从 ↓
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 永远是不允许的,因为从 boolean 到 char 没有强制转换。
基本类型模式在 instanceof 和 switch 中的应用
类型模式将类型测试与条件转换结合起来。这样,如果类型测试成功,则不需要显式的强制转换;如果类型测试失败,则可以在不同的分支中处理未转换的值。当 instanceof 类型测试操作符仅支持引用类型时,自然只允许在 instanceof 和 switch 中使用引用类型模式;现在 instanceof 类型测试操作符支持基本类型,自然也应该允许在 instanceof 和 switch 中使用基本类型模式。
为了实现这一点,我们取消了原始类型不能用于顶级类型模式的限制。因此,繁琐且容易出错的代码
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适用于类型为U的匹配候选者。由于int可以转换为double,因此该switch现在是合法的。 -
无条件性是指在编译时是否已知一个适用的模式将匹配匹配候选者的所运行时可能值。无条件模式不需要运行时检查。
当我们将基本类型模式扩展到适用于更多类型时,我们必须指定它们在哪种类型上是无条件的。如果从
U到T的转换是无条件精确的,则类型为T的基本类型模式对类型为U的匹配候选者是无条件的。这是因为无论输入值如何,无条件精确转换都是安全的。 -
以前,不是
null引用的值v如果可以转换为类型T而不会抛出ClassCastException,则与类型T的类型模式 匹配。当基本类型模式的作用有限时,这种匹配定义已经足够。现在,随着基本类型模式可以广泛使用,匹配被泛化为意味着一个值可以精确地转换为T,这包括抛出ClassCastException以及潜在的信息丢失。
完备性
switch 表达式,或者 case 标签是模式的 switch 语句,需要是穷尽的:switch 块中必须处理选择器表达式的全部可能值。如果 switch 包含一个无条件类型模式,则它是穷尽的;它也可以因为其他原因而成为穷尽的,例如涵盖密封类的所有可能允许的子类型。在某些情况下,即使存在任何 case 都无法匹配的可能运行时值,switch 也可以被认为是穷尽的;在这种情况下,Java 编译器会插入一个合成的 default 子句来处理这些未预料到的输入。关于穷尽性,在模式:穷尽性、无条件性和剩余部分中有更详细的介绍。
随着原始类型模式的引入,我们在确定穷尽性时增加了一条新规则:给定一个 switch,其匹配候选对象是某个原始类型 P 的包装类型 W,如果类型模式 T t 在 P 上无条件精确,则它会穷尽 W。在这种情况下,null 成为剩余部分。在下面的例子中,匹配候选对象是原始类型 byte 的包装类型,并且从 byte 到 int 的转换是无条件精确的。因此,以下 switch 是穷尽的:
Byte b = ...
switch (b) { // exhaustive switch
case int p -> 0;
}
这种行为类似于记录模式的穷尽性处理。
就像 switch 使用模式穷举性来确定各个 case 是否覆盖了所有输入值一样,switch 也使用支配性来确定是否存在某些 case 完全不会匹配任何输入值。
如果一个模式匹配另一个模式所匹配的所有值,则前者支配后者。例如,类型模式 Object o 支配类型模式 String s,因为所有能与 String s 匹配的也都能与 Object o 匹配。在 switch 语句中,如果类型模式 P 支配类型模式 Q,则带有无保护类型模式 P 的 case 标签不能位于带有类型模式 Q 的 case 标签之前。支配的含义不变:如果 T t 对于类型为 U 的匹配候选者是无条件的,则类型模式 T t 支配类型模式 U u。
switch 中扩展的原始类型支持
我们增强了 switch 语句,使其允许选择器表达式的类型为 long、float、double 和 boolean,以及相应的包装类型。
如果选择器表达式的类型是 long、float、double 或 boolean,则用在 case 标签中的任何常量必须与选择器表达式具有相同的类型,或者与其对应的包装类型相同。例如,如果选择器表达式的类型是 float 或 Float,那么任何 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 类型只有两个不同的值,因此列出 true 和 false 情况的 switch 被认为是穷尽的。下面的 switch 是合法的,但如果有一个 default 子句,它就是非法的。
boolean v = ...
switch (v) {
case true -> ...
case false -> ...
// Alternatively: case true, false -> ...
}
未来的工作
在规范了 Java 语言中关于类型比较和模式匹配的规则之后,我们可以考虑引入常量模式。目前,在 switch 中,常量只能作为 case 常量出现,例如这段代码中的 42:
short s = ...
switch (s) {
case 42 -> ...
case int i -> ...
}
常量不能出现在记录模式中,这限制了模式匹配的实用性。例如,以下 switch 是不可能的:
record Box(short s) {}
Box b = ...
switch (b) {
case Box(42) -> ... // Box(42) is not a valid record pattern
case Box(int i) -> ...
}
得益于这里定义的适用性规则,常量可以出现在记录模式中。在 switch 语句中,case Box(42) 将意味着 case Box(int i) when i == 42,因为 42 是类型为 int 的字面量。