JEP 507:模式、instanceof 和 switch 中的基本类型(第三次预览)
JEP 507:模式、instanceof 和 switch 中的基本类型(第三次预览)
原文:JEP 507- Primitive Types in Patterns, instanceof, and switch (Third Preview)
作者:
日期:2025-10-28
| 负责人 | 安耶洛斯·宾普迪西斯(Angelos Bimpoudis) |
|---|---|
| 类型 | 特性 |
| 范围 | 标准版(SE) |
| 状态 | 已完成 / 已交付 |
| 版本 | 25 |
| 组件 | 规范 / 语言 |
| 讨论地址 | amber-dash-dev@openjdk.org |
| 工作量 | M |
| 持续时间 | M |
| 相关内容 | JEP 488:模式、instanceof 和 switch 中的基本类型(第二次预览) JEP 530:模式、instanceof 和 switch 中的基本类型(第四次预览) |
| 审核人 | 亚历克斯·巴克利(Alex Buckley)、布莱恩·戈茨(Brian Goetz) |
| 批准人 | 布莱恩·戈茨(Brian Goetz) |
| 创建时间 | 2025/02/03 12:20 |
| 更新时间 | 2025/10/02 17:52 |
| 问题编号 | 8349215 |
摘要
通过允许在所有模式上下文中使用基本类型来增强模式匹配,并扩展 instanceof 和 switch 以适用于所有基本类型。这是一项 预览语言特性。
历史
此特性最初由 JEP 455(JDK 23)提出,并由 JEP 488(JDK 24)重新预览,内容未变。在此,我们提议进行第三次预览,同样不做修改。
目标
- 允许针对所有类型(无论是基本类型还是引用类型)使用类型模式,以实现统一的数据探索。
- 使类型模式与
instanceof保持一致,并使instanceof与安全类型转换保持一致。 - 允许模式匹配在嵌套和顶级模式上下文中都能使用基本类型。
- 提供易于使用的结构,消除因不安全类型转换而导致信息丢失的风险。
- 继 Java 5(枚举
switch)和 Java 7(字符串switch)中对switch的增强之后,允许switch处理任何基本类型的值。
非目标
- 向 Java 语言添加新的转换类型并非本特性的目标。
动机
与基本类型相关的多个限制在使用模式匹配、instanceof 和 switch 时会产生阻碍。消除这些限制将使 Java 语言更加统一,更具表现力。
switch 的模式匹配不支持基本类型模式
第一个限制是,switch 的模式匹配(JEP 441)不支持基本类型模式,即指定基本类型的类型模式。仅支持指定引用类型的类型模式,如 case Integer i 或 case 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;
}
支持基本类型模式还能让守卫(guards)检查匹配的值:
switch (x.getYearlyFlights()) {
case 0 -> ...;
case 1 -> ...;
case 2 -> issueDiscount();
case int i when i >= 100 -> issueGoldCard();
case int i -> ...当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; // 不可避免的(且可能有数据丢失的!)类型转换
}
换句话说,基本类型模式可以嵌套在记录模式中,但具有不变性:模式中的基本类型必须与记录组件的基本类型相同。无法通过 instanceof JsonNumber(int age) 分解 JsonNumber 并让编译器自动将 double 组件缩小为 int 类型。
这种限制的原因在于缩小转换可能会有数据丢失:在运行时,double 组件的值可能太大,或者精度太高,不适合 int 变量。然而,模式匹配的一个关键优势在于它可以通过不匹配来自动拒绝非法值。如果 JsonNumber 的 double 组件太大或精度太高,无法安全地缩小为 int 类型,那么 instanceof JsonNumber(int age) 可以直接返回 false,让程序在另一个分支中处理较大的 double 组件。
通过支持基本类型模式,我们可以消除这一限制。模式匹配可以在顶级以及嵌套在记录模式中时,保障将值转换为基本类型时可能有数据丢失的缩小转换。由于任何 double 类型都可以转换为 int 类型,基本类型模式 int a 将适用于 JsonNumber 中 double 类型的相应组件。当且仅当 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(); // 潜在的数据丢失悄无声息
同时,将 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) { // i的值适合存储在byte中
... (byte)i... // 需要传统的类型转换
}
对 instanceof 的这种增强恢复了 instanceof T 和 instanceof T t 语义之间的一致性,如果我们只在一种上下文中允许基本类型而在另一种上下文中不允许,这种一致性就会丧失。
最后,取消 switch 只能接受 byte、short、char 和 int 值,而不能接受 boolean、float、double 或 long 值的限制会很有帮助。
对 boolean 值进行 switch 操作将是三元条件运算符(?:)的有用替代,因为 boolean 的 switch 可以包含语句以及表达式。例如,以下代码使用 boolean 的 switch 在值为 false 时执行一些日志记录:
startProcessing(OrderStatus.NEW, switch (user.isLoggedIn()) {
case true -> user.id();
case false -> { log("Unrecognized user"); yield -1; }
});
对 long 值进行 switch 操作将允许 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,那么基本类型之间的转换就是无条件精确的。此外,装箱转换和拓宽引用转换是无条件精确的。
下表表示基本类型之间允许的转换。无条件精确的转换用符号 ɛ 表示。符号 ≈ 表示标识转换,ω 表示拓宽基本转换,η 表示缩小基本转换,ωη 表示拓宽和缩小基本转换。符号 — 表示不允许转换。
| 到 → | byte | short | char | int | long | float | double | boolean |
|---|---|---|---|---|---|---|---|---|
| 从 ↓ | ||||||||
| byte | ≈ | ɛ | ωη | ɛ | ɛ | ɛ | ɛ | — |
| short | η | ≈ | η | ɛ | ɛ | ɛ | ɛ | — |
| char | η | η | ≈ | ɛ | ɛ | ɛ | ɛ | — |
| int | η | η | η | ≈ | ɛ | ω | ɛ | — |
| long | η | η | η | η | ≈ | ω | ω | — |
| float | η | η | η | η | η | ≈ | ɛ | — |
| double | η | η | η | η | η | η | ≈ | — |
| boolean | — | — | — | — | — | — | — | ≈ |
将此表与 《Java 语言规范》(JLS)§5.5 中的等效表进行比较,可以看出,JLS §5.5 中许多由 ω 允许的转换在上面被“升级”为无条件精确的 ɛ。
作为安全强制转换前提条件的 instanceof
传统上,使用 instanceof 进行的类型测试仅限于引用类型。instanceof 的经典含义是一种前提条件检查,它会提出这样的问题:将这个值强制转换为该类型是否安全且有用?这个问题对于基本类型来说甚至比对于引用类型更为相关。对于引用类型而言,如果不小心省略了检查,那么执行不安全的强制转换可能不会造成损害:将会抛出 ClassCastException 异常,并且转换不当的值将无法使用。相比之下,对于基本类型,由于没有方便的安全检查方法,执行不安全的强制转换可能会导致难以察觉的错误。它不会抛出异常,而是可能会无声地丢失诸如大小、符号或精度之类的信息,从而使转换不当的值进入程序的其余部分。
为了使 instanceof 类型测试操作符能够用于基本类型,我们取消了相关限制(《Java 语言规范》第 15.20.2 节),即左侧操作数的类型必须是引用类型,并且右侧操作数必须指定一个引用类型。类型测试操作符变为:
InstanceofExpression:
RelationalExpression instanceof Type
...
在运行时,我们通过使用精确转换将 instanceof 扩展到基本类型:如果左侧的值可以通过精确转换转换为右侧的类型,那么将该值强制转换为该类型就是安全的,并且 instanceof 返回 true。
以下是扩展后的 instanceof 如何保障强制转换的一些示例。无条件精确转换无论输入值是什么都返回 true;所有其他转换都需要进行运行时测试,测试结果如下所示。
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 永远不被允许,因为不存在从 boolean 到 char 的强制转换。
instanceof 和 switch 中的基本类型模式
类型模式将类型测试与条件转换结合在一起。如果类型测试成功,这就避免了显式强制转换的需要,而如果类型测试失败,则可以在不同的分支中处理未转换的值。当 instanceof 类型测试操作符仅支持引用类型时,在 instanceof 和 switch 中只允许引用类型模式是很自然的;既然 instanceof 类型测试操作符现在支持基本类型,那么在 instanceof 和 switch 中允许基本类型模式也是自然而然的。
为了实现这一点,我们取消了基本类型不能用于顶级类型模式的限制。结果,原本繁琐且容易出错的代码:
int i = 1000;
if (i instanceof byte) { // false -- i无法精确转换为byte
byte b = (byte)i; // 可能有数据丢失
... b ...
}
可以写成:
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现在是合法的。 - 无条件性 指的是在编译时是否可以知道一个适用的模式将匹配匹配候选的所有可能运行时的值。无条件模式不需要运行时检查。
当我们将基本类型模式扩展为适用于更多类型时,我们必须指定它们在哪些类型上是无条件的。如果从U到T的转换是无条件精确的,那么对于类型T的基本类型模式对于类型为U的匹配候选就是无条件的。这是因为无条件精确转换无论输入值如何都是安全的。 - 以前,如果值
v不是null引用,并且v可以在不抛出ClassCastException的情况下强制转换为类型T,那么它就与类型T的类型模式相匹配。当基本类型模式的作用有限时,这种匹配定义就足够了。既然现在基本类型模式可以广泛使用,匹配就被泛化为意味着一个值可以精确地强制转换为T,这涵盖了抛出ClassCastException以及潜在的信息丢失情况。
穷举性
switch 表达式,或者其 case 标签为模式的 switch 语句,必须是 穷举的:选择器表达式的所有可能值都必须在 switch 块中得到处理。如果 switch 包含一个无条件类型模式,那么它就是穷举的;它也可能由于其他原因而具有穷举性,例如涵盖了密封类的所有可能允许的子类型。在某些情况下,即使存在可能的运行时值不会被任何 case 匹配,switch 也可以被认为是穷举的;在这种情况下,Java 编译器会插入一个合成的 default 子句来处理这些意外输入。有关穷举性的更详细内容,请参见 《模式:穷举性、无条件性和余数》。
随着基本类型模式的引入,我们在确定穷举性方面添加了一条新规则:给定一个 switch,其匹配候选是某个基本类型 P 的包装类型 W,如果 T 对于 P 是无条件精确的,那么类型模式 T t 就穷举了 W。在这种情况下,null 成为余数的一部分。在下面的例子中,匹配候选是基本类型 byte 的包装类型,并且从 byte 到 int 的转换是无条件精确的。因此,下面的 switch 是穷举的:
Byte b = ...
switch (b) { // 穷举的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 类型的浮点文字量(《Java 语言规范》第 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 -> ... // 错误:重复的标签
default -> ...
}
由于 boolean 类型只有两个不同的值,列出 true 和 false 两种情况的 switch 被认为是穷举的。以下 switch 语句是合法的,但如果有 default 子句就会不合法。
boolean v = ...
switch (v) {
case true -> ...
case false -> ...
// 或者: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) 不是有效的记录模式
case Box(int i) -> ...
}
由于这里定义的适用性规则,常量可以被允许出现在记录模式中。在 switch 中,case Box(42) 的意思是 case Box(int i) when i == 42,因为 42 是 int 类型的文字量。