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
的字面量。