JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview) | 模式、instanceof 和 switch 中的基本类型(预览)
摘要
通过在所有模式上下文中允许基本类型模式,并扩展 instanceof 和 switch 以支持所有基本类型,来增强模式匹配。这是一个 预览语言特性。
目标
允许对所有类型(无论是基本类型还是引用类型)使用类型模式,以实现统一的数据探索。
使类型模式与
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;
}支持基本类型模式还将允许保护子句检查匹配的值:
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;我们可以传递一个如 30 的 int,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 组件。
这是模式匹配对引用类型模式已经如何工作的方式。例如:
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 来尝试将 Box 与具有 RedBall 组件或 BlueBall 组件的 Box 进行匹配。记录模式 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(); // 可能会默默丢失信息同时,将 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 语言中关于基本类型使用的一小部分规则来实现这些更改:
移除
instanceof和switch构造中对基本类型和基本类型模式的限制;扩展
switch以处理所有基本类型字面量的常量情况;确定一种类型转换到另一种类型转换是否安全,这涉及对要转换的值以及转换的源类型和目标类型的了解。
转换的安全性
如果转换过程中没有信息丢失,则该转换是 精确的。转换是否精确取决于所涉及的类型对和输入值:
对于某些类型对,编译时已知从第一个类型到第二个类型的转换保证不会丢失任何值的信息。这种转换被称为 无条件精确。对于无条件精确的转换,运行时无需执行任何操作。示例包括从
byte到int、从int到long以及从String到Object的转换。对于其他类型对,需要进行运行时测试以检查是否可以将值从第一个类型转换为第二个类型而不会丢失信息,或者在执行强制转换时不会引发异常。如果不会发生信息丢失或异常,则转换是精确的;否则,转换不是精确的。可能精确的转换示例包括从
long到int和从int到float的转换,其中通过在运行时使用数值相等性(==)或 表示等价性 来检测精度损失。从Object到String的转换也需要进行运行时测试,并且转换是否精确取决于输入值在动态上是否为String。
简而言之,如果从一个整数类型转换到另一个整数类型,或者从一个浮点类型转换到另一个浮点类型,或者从 byte、short 或 char 转换到浮点类型,或者从 int 转换到 double,则基本类型之间的转换是无条件精确的。此外,装箱转换和引用类型的加宽转换也是无条件精确的。
下表表示基本类型之间允许的转换。无条件精确转换用符号 ɛ 表示。符号 ≈ 表示恒等转换,ω 表示基本类型加宽转换,η 表示基本类型缩窄转换,而 ωη 表示加宽和缩窄的基本类型转换。符号 — 表示不允许转换。
| To → | byte | short | char | int | long | float | double | boolean |
|---|---|---|---|---|---|---|---|---|
| From ↓ | ||||||||
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(无条件精确)
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 cannot be converted exactly to byte
byte b = (byte)i; // potentially lossy
... b ...
}可以写为
if (i instanceof byte b) {
... b ... // 无信息丢失
}因为 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 表达式或 switch 语句(其 case 标签是模式)必须是 穷尽的:选择器表达式的所有可能值都必须在 switch 块中处理。如果 switch 包含无条件类型模式,则它是穷尽的;它也可能因为其他原因而穷尽,例如覆盖密封类的所有可能的允许子类型。在某些情况下,即使存在运行时值不会与任何 case 匹配,switch 也可能被视为穷尽;在这种情况下,Java 编译器会插入一个合成的 default 子句来处理这些未预料的输入。穷尽性在 Patterns: Exhaustiveness, Unconditionality, and Remainder 中有更详细的介绍。
随着基本类型模式的引入,我们在确定穷尽性时增加了一条新规则:给定一个匹配候选项为某个基本类型 P 的包装类型 W 的 switch,如果 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 的浮点字面量(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 -> ... // 错误:重复标签
default -> ...
}由于 boolean 类型只有两个不同的值,因此列出 true 和 false 情况的 switch 被视为详尽无遗的。以下 switch 是合法的,但如果存在 default 子句,则将是非法的。
boolean v = ...
switch (v) {
case true -> ...
case false -> ...
// 或者:case true, false -> ...
}