跳到主要内容

JEP 455:模式、instanceof 和 switch 中的原始类型(预览版)

概括

通过允许所有模式上下文中的基本类型模式来增强模式匹配,并扩展instanceofswitch使用所有基本类型。这是预览语言功能

目标

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

  • 将文字图案与 对齐instanceof,并instanceof与安全铸造对齐。

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

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

  • switch遵循Java 5 (enum switch) 和 Java 7 (string switch)中的增强功能,允许switch处理任何基本类型的值。

非目标

  • 向 Java 语言添加新类型的转换并不是目标。

动机

instanceof在使用模式匹配、和时,与基元类型相关的多个限制会带来摩擦switch。消除这些限制将使 Java 语言更加统一、更具表现力。

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

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

通过对 中原始类型模式的支持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以实现最大的灵活性。但是,我们double在创建记录时不需要传递a JsonNumber;我们可以传递一个int诸如30,Java 编译器会自动将 扩展intdouble

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

JsonNumber不幸的是,如果我们希望用记录模式来分解 a,Java 编译器并不那么乐于助人。由于JsonNumber是用double组件声明的,因此我们必须将 a 分解JsonNumberdouble,并手动转换为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
}

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

此限制的原因是缩小可能是有损的:double对于变量来说,运行时分量的值可能太大,或者精度太高int。然而,模式匹配的一个主要好处是它通过简单地不匹配来自动拒绝非法值。如果doublea 的组件JsonNumber太大或太精确而无法安全地缩小到 an 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可用于尝试将 aBoxRedBall组件或BlueBall组件进行匹配。仅当运行时为 a并且其组件可以缩小为 时,记录模式Box(RedBall rb)才匹配;类似地,仅当其分量可以缩小到时才匹配。b``Box``o``RedBall``Box(BlueBall bb)``o``BlueBall

在记录模式中,原始类型模式应该像引用类型模式一样顺利工作,JsonNumber(int age)即使相应的记录组件是除int.这将消除在匹配模式后进行冗长且可能有损的转换的需要。

模式匹配instanceof不支持基本类型

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

instanceof原始类型模式在 中和在 中一样有用switch。广义上来说,其目的instanceof是测试一个值是否可以安全地转换为给定的类型;这就是为什么我们总是instanceof近距离观察和投射操作。此测试对于基元类型至关重要,因为将基元值从一种类型转换为另一种类型时可能会发生信息丢失。

例如,将int值转换为 afloat是由赋值语句自动执行的,即使它可能有损 — 并且开发人员不会收到任何警告:

int getPopulation() {...}
float pop = getPopulation(); // silent potential loss of information

同时,将int值转换为 abyte是通过显式强制转换执行的,但强制转换可能是有损的,因此必须在其之前进行费力的范围检查:

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) 可以安全地转换为原始类型模式中的类型,则模式匹配并且转换结果立即可用 (popb)。但是,如果转换会丢失信息,则模式不匹配,程序应该在不同的分支中处理无效输入。

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最后,解除可以采用byteshortcharint值但不能采用booleanfloatdoublelong值的限制会很有帮助。

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

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,我们将:

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

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

  3. 进一步增强instanceof构造,以便当用于类型测试而不是模式匹配时,它可以针对所有类型进行测试,而不仅仅是引用类型。这将扩展instanceof的当前角色(作为引用类型安全转换的前提条件),以应用于所有类型。

    更广泛地说,这意味着instanceof可以保护所有转换,无论匹配候选者正在对其类型进行测试(例如,x instanceof int、 或y instanceof String),还是对其值进行匹配(例如,x instanceof int i、 或y instanceof String s)。

  4. 进一步增强该switch构造,使其适用于所有原始类型,而不仅仅是完整原始类型的子集。

我们通过更改 Java 语言中管理基本类型使用的少量规则来实现这些更改:

  • instanceof删除and构造中对原始类型和原始类型模式的限制switch

  • 扩展switch以处理所有原始类型的常量情况;和

  • 表征从一种类型到另一种类型的转换何时是安全的,这涉及要转换的值以及转换的源类型和目标类型的知识。

转换的安全性

如果没有发生信息丢失,则转换是_准确的_。转换是否准确取决于所涉及的类型对以及输入值:

  • 对于某些对,在编译时已知从第一类型到第二类型的转换保证不会丢失任何值的信息。据说该转换是_无条件精确的_。在运行时无需执行任何操作即可实现无条件精确转换。示例包括byteto intinttolongStringto Object

  • 对于其他对,需要运行时测试来检查值是否可以从第一种类型转换为第二种类型而不丢失信息,或者是否要执行强制转换而不引发异常。如果不发生信息丢失或异常,则转换是准确的;否则,转换不准确。可能精确的转换示例包括longtointintto ,其中分别通过使用数值相等 ( ) 或表示等价float在运行时检测精度损失。从 转换为也需要运行时测试,转换是否准确取决于输入值是否动态为.==``Object``String``String

简而言之,如果基本类型之间的转换从一种整型类型扩大到另一种类型,或者从一种浮点类型扩大到另一种浮点类型,或者从byte、 、shortchar扩大到浮点类型,或者从 扩大int到,则它是无条件精确的double。此外,装箱转换和加宽引用转换是无条件精确的。

下表表示基本类型之间允许的转换。无条件精确转换用符号 表示 ɛ。符号 表示恒等转换,ω 表示加宽基元转换,η 表示缩小基元转换,ωη 表示加宽和缩小基元转换。该符号 表示不允许转换。

至 →

byte

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将抛出 A 并且不正确的强制转换值将不可用。相反,对于原始类型,没有方便的方法来检查安全性,执行不安全的强制转换可能会导致微妙的错误。它不会抛出异常,而是会默默地丢失大小、符号或精度等信息,从而允许不正确转换的值流入程序的其余部分。

为了在类型测试运算符中启用基本类型,我们删除了左侧操作数的类型必须是引用类型并且右侧操作数必须指定引用类型的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 instanceof char永远不允许 ifbboolean变量,因为没有从boolean到 的强制转换char

instanceof和中的原始类型模式switch

类型模式将类型测试与条件转换合并。如果类型测试成功,这可以避免显式强制转换的需要,而如果类型测试失败,则可以在不同的分支中处理未强制转换的值。当类型测试运算符仅支持引用类型时,很自然, andinstanceof中只允许引用类型模式;既然类型测试运算符支持原始类型,那么很自然地允许和中的原始类型模式。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 instanceof byte,如果是,则转换i为该byte值并将其绑定到b”。

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

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

    现在,如果可以在没有未经检查的警告的情况下将 a 转换为类型,则类型模式T t适用于类型的匹配候选者。由于可以转换为,所以现在这是合法的。U``U``T``int``double``switch

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

    当我们扩展原始类型模式以适用于更多类型时,我们必须指定它们对哪些类型是无条件的。如果从到 的转换无条件精确,则type 的原始类型模式对于Ttype 的匹配候选者是无条件的。这是因为无论输入值如何,无条件精确转换都是安全的。U``U``T

  • v以前,不是引用的null值与if类型的类型模式_匹配_,可以在不抛出.当原始类型模式的作用有限时,这种匹配定义就足够了。既然原始类型模式可以被广泛使用,匹配被概括为意味着可以将值精确地转换为,这涵盖了抛出 a以及潜在的信息丢失。T``v``T``ClassCastException``T``ClassCastException

详尽性

一个switch表达式,或者一个标签为模式switch的语句case,需要是_详尽的_:选择器表达式的所有可能值必须在块中处理switchswitch如果 A 包含无条件类型模式,则A是详尽的;由于其他原因,它也可能是详尽的,例如涵盖密封类的所有可能允许的子类型。在某些情况下,switch即使存在可能无法与任何case;匹配的运行时值, a 也可以被视为详尽无遗。在这种情况下,Java 编译器会插入一个合成default子句来处理这些意外的输入。模式:穷举性、无条件性和余数中更详细地介绍了穷举性。

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

Byte b = ...
switch (b) { // exhaustive switch
case int p -> 0;
}

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

正如switch使用模式穷举性来确定案例是否涵盖所有输入值一样,switch使用优势来确定是否存在与任何输入值不匹配的案例。

如果一种模式与另一种模式匹配的所有值相匹配,则该模式_支配_另一种模式。例如,类型模式Object o主导类型模式,String s因为所有匹配的内容String s也将匹配Object o。在 a 中,如果占主导地位,则具有不受保护的类型模式的标签位于具有类型模式的 case 标签之前switch是非法的。支配的含义不变:如果对类型 的匹配候选无条件,则类型模式支配类型模式。case``P``Q``P``Q``T t``U u``T t``U

扩展了原始支持switch

我们增强了构造以允许类型、、和以及相应的装箱类型switch的选择器表达式。long``float``double``boolean

如果选择器表达式的类型为longfloatdoubleboolean,则 case 标签中使用的任何常量必须与选择器表达式或其相应的装箱类型具有相同的类型。例如,如果选择器表达式的类型是float或 ,Float则任何常量都必须是类型的case浮点文字 ( JLS §3.10.2float ) 。此限制是必需的,因为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类型只有两个不同的值,switch因此列出truefalse情况的 a 被认为是详尽的。以下switch是合法的,但如果有条款的话就是非法的default

boolean v = ...
switch (v) {
case true -> ...
case false -> ...
// Alternatively: case true, false -> ...
}