JEP 427:开关的模式匹配(第三次预览)
概括
switch
通过表达式和语句的模式匹配增强 Java 编程语言。扩展模式匹配允许switch
针对多个模式测试表达式,每个模式都有一个特 定的操作,以便可以简洁、安全地表达复杂的面向数据的查询。这是预览语言功能。
历史
模式匹配由JEP 406switch
提议作为预览功能并在JDK 17中提供,并由JEP 420提议作为第二个预览并在JDK 18中提供。此 JEP 提出了第三个预览版,并根据持续的经验和反馈进行了进一步的改进。
自第二次预览以来的主要变化是:
-
受保护的模式被替换为
when
switch 块中的子句。 -
当选择器表达式的值为 时,模式切换的运行时语义
null
与传统切换语义更加一致。
目标
-
switch
通过允许模式出现在case
标签中来扩展表达式和语句的表现力和适用性。 -
switch
在需要时允许放松历史上的零敌意。 -
switch
通过要求模式switch
语句涵盖所有可能的输入值来提高语句的安全性。 -
确保所有现有
switch
表达式和语句继续编译而不进行任何更改并以相同的语义执行。
动机
在 Java 16 中,JEP 394扩展了该instanceof
运算符以采用_类型模式_并执行_模式匹配_。这个适度的扩展允许简化熟悉的 instanceof-and-cast 习惯用法:
// Old code
if (o instanceof String) {
String s = (String)o;
... use s ...
}
// New code
if (o instanceof String s) {
... use s ...
}
我们经常想要将一个变量与o
多个替代方案进行比较。 Java 支持switch
语句和switch
表达式(JEP 361 )的多路比较,但不幸的switch
是非常有限。您只能打开几种类型的值 - 整型原始类型(不包括long
)、其相应的装箱形式、枚举类型等String
- 并且只能测试与常量的精确相等。我们可能想使用模式来针对多种可能性测试同一变量,对每种可能性采取特定的操作,但由于现有的switch
不支持,我们最终会得到一系列if...else
测试,例如:
static String formatter(Object o) {
String formatted = "unknown";
if (o instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
formatted = String.format("double %f", d);
} else if (o instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
该代码受益于使用模式instanceof
表达式,但它远非完美。首先也是最重要的,这种方法可以隐藏编码错误,因为我们使用了过于通用的控制结构。目的是为链formatted
的每个分支分配一些内容if...else
,但没有任何东西可以使编译器识别和验证此不变量。如果某个块(也许是很少执行的块)没有分配给formatted
,我们就会遇到一个错误。 (声明formatted
为空白本地至少会在这项工作中利用编译器的明确赋值分析,但开发人员并不总是编写这样的声明。)此外,上述代码不可优化;如果没有编译器英雄,它将具有_O_ ( n ) 时间复杂度,即使底层问题通常是_O_ (1)。
但switch
对于图案搭配来说却是绝配!如果我们扩展switch
语句和表达式以适用于任何类型,并允许使用case
模式标签而不仅仅是常量,那么我们可以更清晰可靠地重写上面的代码:
static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
}
其语义switch
很明确:case
如果选择器表达式的值与o
模式匹配,则应用带有模式的标签。 (为了简洁起见,我们显示了一个switch
表达式,但也可以显示一个switch
语句;switch 块(包括case
标签)将保持不变。)
这段代码的意图更加清晰,因为我们使用了正确的控制结构:我们是说,“参数o
最多匹配以下条件之一,找出并评估相应的手臂。”作为奖励,它是可优化的;在这种情况下,我们更有可能能够在_O_ (1) 时间内执行调度。
模式开关和空值
传统上,如果选择器表达式的计算结果为 ,则switch
语句和表达式会抛出异常,因此必须在 之外进行测试:NullPointerException``null``null``switch
static void testFooBar(String s) {
if (s == null) {
System.out.println("Oops!");
return;
}
switch (s) {
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
switch
当仅支持少数引用类型时,这是合理的。然而,如果switch
允许任何类型的选择器表达式,并且case
标签可以具有类型模式,那么独立null
测试感觉像是任意的区别,并且会带来不必要的样板文件和出错的机会。最好通过允许新的案例标签将null
测试集成到中:switch``null
static void testFooBar(String s) {
switch (s) {
case null -> System.out.println("Oops");
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
switch
当选择器表达式的值为 时,其行为null
始终由其标签决定case
。使用 a case null
,switch
执行与该标签关联的代码;如果没有 a case null
,则switch
抛出NullPointerException
,就像以前一样。 (为了保持与当前语义的向后兼容性switch
,default
标签与选择器不匹配null
。)
我们可能希望null
与另一个case
标签结合。例如,在以下代码中,标签case null, String s
既匹配null
值又匹配所有String
值:
static void testStringOrNull(Object o) {
switch (o) {
case null, String s -> System.out.println("String: " + s);
default -> System.out.println("Something else");
}
}
案例细化
对模式的实验switch
表明,希望改进模式标签所体现的测试是很常见的。例如,考虑以下切换值的代码Shape
:
class Shape {}
class Rectangle extends Shape {}
class Triangle extends Shape { int calculateArea() { ... } }
static void testTriangle(Shape s) {
switch (s) {
case null:
break;
case Triangle t:
if (t.calculateArea() > 100) {
System.out.println("Large triangle");
break;
}
default:
System.out.println("A shape, possibly a small triangle");
}
}
此代码的目的是为大三角形(面积超过 100 的三角形)提供特殊情况,为其他所有三角形(包括小三角形)提供默认情况。然而,我们不能用单一模式直接表达这一点。我们首先必须编写一个case
匹配所有三角形的标签,然后将三角形面积的测试相当不舒服地放在相应的语句组中。然后,当三角形的面积小于 100 时,我们必须使用 drop-through 来获得正确的行为。(请注意语句break
在if
块内的小心放置。)
这里的问题是,使用单一模式来区分情况并不能超出单一条件——我们需要某种方法来表达对模式的细化。一种方法是引入书面_的受保护模式_p && b
,允许通过任意布尔表达式对模式p
进行细化b
。
我们在此 JEP 的前身中实现了受保护的模式。根据经验和反馈,我们建议改为允许when
switch 块中的子句指定模式标签的防护,例如case Triangle t when t.calculateArea() > 100
。我们将这样的模式标签称为_受保护的_模式标签,并将布尔表达式称为_Guard_。
通过这种方法,我们可以重新访问testTriangle
代码来直接表达大三角形的特殊情况。这消除了在语句中使用 drop-through 的情况switch
,这又意味着我们可以享受简洁的箭头式 ( ->
) 规则:
static void testTriangle(Shape s) {
switch (s) {
case null ->
{ break; }
case Triangle t
when t.calculateArea() > 100 ->
System.out.println("Large triangle");
default ->
System.out.println("A shape, possibly a small triangle");
}
}
如果 的值与s
模式匹配,则采用第一个子句Triangle t
_,并且_随后防护的t.calculateArea() > 100
计算结果为true
。 (守卫能够使用case
标签中模式声明的任何模式变量。)
当应用程序需求发生变化时,使用switch
它可以轻松理解和更改案例标签。例如,我们可能想从 默认路径中分割出三角形;我们可以通过使用两个 case 子句来做到这一点,一个带有保护,另一个不带:
static void testTriangle(Shape s) {
switch (s) {
case null ->
{ break; }
case Triangle t
when t.calculateArea() > 100 ->
System.out.println("Large triangle");
case Triangle t ->
System.out.println("Small triangle");
default ->
System.out.println("Non-triangle");
}
}
描述
我们switch
通过三种方式增强陈述和表达:
-
扩展
case
标签以包含模式和null
常量, -
switch
扩大语句和表达式的选择器表达式允许的类型范围switch
,以及 -
允许可选
when
子句跟随case
标签。
为了方便起见,我们还引入了_括号模式_。
开关标签中的模式
主要的增强是引入了一个新的case p
开关标签,其中p
是一个模式。本质switch
没有改变:将选择器表达式的值与开关标签进行比较,选择标签之一,并执行或评估与该标签关联的代码。现在的区别在于,对于case
带有模式的标签,选择的标签是由模式匹配的结果决定的,而不是由相等性测试决定的。例如,在以下代码中, 的值与o
模式匹配,并且计 算Long l
与标签关联的表达式:case Long l
Object o = 123L;
String formatted = switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
成功的模式匹配后,我们通常会进一步测试匹配的结果。这可能会导致代码变得繁琐,例如:
static void test(Object o) {
switch (o) {
case String s:
if (s.length() == 1) { ... }
else { ... }
break;
...
}
}
不幸的是,所需的测试(即长度为 1 的o
a String
)被分割在模式case
标签和以下if
语句之间。
为了解决这个问题,我们通过在模式标签后支持可选的保护来引入_保护模式标签_。这允许重写上面的代码,以便将所有条件逻辑提升到 switch 标签中:
static void test(Object o) {
switch (o) {
case String s when s.length() == 1 -> ...
case String s -> ...
...
}
}
第一个子句匹配 if o
is aString
_且_长度为 1。第二个子句匹配 if o
is aString
任意长度。
只有图案标签才能有防护。例如,编写带有常量case
和保护的标签是无效的,例如case "Hello" when RandomBooleanExpression()
。
有时我们需要将模式括起来以提高可读性。因此,我们扩展了模式语言以支持编写_的括号模式_(p)
,其中p
是模式。带括号的模式(p)
引入了由子模式引入的模式变量p
。(p)
如果值与模式匹配,则该值与带括号的模式匹配p
。
支持以下模式时需要考虑四个主要的语言设计领域switch
:
- 增强类型检查
switch
表达式和陈述的详尽性- 模式变量声明的范围
- 处理
null
1. 增强类型检查
1a.选择器表达式输入
支持 in 模式switch
意味着我们可以放宽当前对选择器表达式类型的限制。目前,法线的选择器表达式的类型switch
必须是整型原始类型(不包括long
)、相应的装箱形式(即 、Character
、Byte
或Short
)Integer
、String
或enum
类型。我们对此进行了扩展,并要求选择器表达式的类型是整型原始类型(不包括long
)或任何引用类型。
例如,在以下模式中,switch
选择器表达式o
与涉及类类型、枚举类型、记录类型和数组类型以及null
case
标签和 a 的类型模式匹配default
:
record Point(int i, int j) {}
enum Color { RED, GREEN, BLUE; }
static void typeTester(Object o) {
switch (o) {
case null -> System.out.println("null");
case String s -> System.out.println("String");
case Color c -> System.out.println("Color: " + c.toString());
case Point p -> System.out.println("Record class: " + p.toString());
case int[] ia -> System.out.println("Array of ints of length" + ia.length);
default -> System.out.println("Something else");
}
}
switch 块中的每个case
标签都必须与选择器表达式兼容。对于case
带有模式的标签(称为_模式标签) ,我们使用__表达式与模式的兼容性_的现有概念(JLS §14.30.1)。
1b.图案标签的主导地位
支持模式标签意味着现在可以将多个标签应用于选择器表达式的值(以前,所有 case 标签都是不相交的)。例如,标签case String s
和case CharSequence cs
都可以应用于类型 的值String
。
要解决的第一个问题是准确决定在这种情况下应应用哪个标签。我们没有尝试复杂的最佳拟合方法,而是采用更简单的语义:选择出现在适用于某个值的 switch 块中的第一个模式标签。
static void first(Object o) {
switch (o) {
case String s ->
System.out.println("A string: " + s);
case CharSequence cs ->
System.out.println("A sequence of length " + cs.length());
default -> {
break;
}
}
}
在此示例中,如果 的值o
是类型String
,则将应用第一个模式标签;如果它是类型CharSequence
但不是类型String
,则将应用第二个模式标签。
但是如果我们交换这两个模式标签的顺序会发生什么?
static void error(Object o) {
switch (o) {
case CharSequence cs ->
System.out.println("A sequence of length " + cs.length());
case String s -> // Error - pattern is dominated by previous pattern
System.out.println("A string: " + s);
default -> {
break;
}
}
}
现在,如果 的值o
属于模式标签类型String
,CharSequence
因为它首先出现在开关块中。模式String
标签是不可访问的,因为选择器表达式没有值会导致它被选择。与无法访问的代码类似,这被视为程序员错误并导致编译时错误。
更准确地说,我们说第一个模式标签case CharSequence cs
_支配_第二个模式标签,case String s
因为与该模式匹配的每个值String s
也与该模式匹配CharSequence cs
,但反之则不然。这是因为第二个模式 的类型String
是第一个模式 的类型的子类型CharSequence
。
未受保护的模式标签支配具有相同模式的受保护的模式标签。例如,(未受保护的)模式标签case String s
支配受保护的模式标签case String s when s.length() > 0
,因为与模式标签匹配的每个值都case String s when s.length() > 0