跳到主要内容

JEP 420:switch 模式匹配(第二次预览)

QWen Max 中英对照 JEP 420: Pattern Matching for switch (Second Preview)

总结

通过为 switch 表达式和语句添加模式匹配,以及对模式语言的扩展来增强 Java 编程语言。将模式匹配扩展到 switch 可以使一个表达式根据多个模式进行测试,每个模式都有特定的操作,从而能够简洁且安全地表达以数据为导向的复杂查询。这是 JDK 18 中的一个 预览语言功能

历史

JEP 406 提出了 switch 的模式匹配,并在 JDK 17 中作为 预览功能 提供。本 JEP 建议在 JDK 18 中对该功能进行第二次预览,并根据经验和反馈进行了小的改进。

自第一个预览版以来的增强功能包括:

  • 为了提高可读性,主导性检查现在会强制要求常量的 case 标签出现在同类型的受保护模式之前(参见下方 1b);以及

  • 对于 switch 块的穷尽性检查,在密封层次结构中变得更加精确,其中允许的直接子类仅扩展(泛型)sealed 超类的实例化(参见下方 2)。

目标

  • 通过允许在 case 标签中出现模式,扩展 switch 表达式和语句的表现力与适用性。

  • 在需要时,允许放宽 switch 历史上的对空值的严格限制。

  • 引入两种新的模式:带保护条件的模式,允许使用任意布尔表达式来细化模式匹配逻辑;以及带括号的模式,用于解决一些解析歧义问题。

  • 确保所有现有的 switch 表达式和语句无需任何更改即可继续编译,并以相同的语义执行。

  • 不引入与传统 switch 结构分离的、具有模式匹配语义的类似 switch 的新表达式或语句。

  • case 标签为模式时,不使 switch 表达式或语句的行为与 case 标签为传统常量时有所不同。

动机

在 Java 16 中,JEP 394 扩展了 instanceof 运算符以支持 类型模式 并实现 模式匹配。这一适度的扩展简化了常见的 instanceof 和强制转换的习惯用法:

// Old code
if (o instanceof String) {
String s = (String)o;
... use s ...
}

// New code
if (o instanceof String s) {
... use s ...
}
java

我们经常需要将 o 这样的变量与多个选项进行比较。Java 通过 switch 语句以及从 Java 14 开始支持的 switch 表达式(JEP 361)来实现多路比较,但遗憾的是,switch 的功能非常有限。你只能根据少数几种类型的值进行切换 —— 数字类型、枚举类型和 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;
}
java

这段代码受益于使用模式 instanceof 表达式,但远非完美。首先,这种方法允许编码错误隐藏起来,因为我们使用了一个过于通用的控制结构。我们的意图是在 if...else 链的每个分支中为 formatted 赋值,但没有任何机制能够让编译器识别并验证这一不变量。如果某些代码块(可能是很少执行的块)没有对 formatted 进行赋值,就会产生一个 bug。(将 formatted 声明为一个空白局部变量至少可以让编译器的确定性赋值分析发挥作用,但这样的声明并不总是被编写出来。)此外,上述代码无法优化;即使底层问题通常是 O(1) 的时间复杂度,但在缺乏编译器优化的情况下,它的时间复杂度会是 O(n)。

但是 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();
};
}
java

这个 switch 的语义很明确:如果选择器表达式 o 的值与模式匹配,那么带有该模式的 case 标签就会匹配该值。(为了简洁起见,我们展示了一个 switch 表达式,但也可以展示一个 switch 语句;包括 case 标签在内的 switch 块将保持不变。)

这段代码的意图更加清晰,因为我们使用了正确的控制结构:我们说的是,“参数 o 最多匹配以下条件之一,找出它并评估相应的分支。” 作为额外的好处,它是可优化的;在这种情况下,我们更有可能以 O(1) 的时间复杂度执行调度。

模式匹配与 null

传统上,如果选择器表达式的结果为 nullswitch 语句和表达式会抛出 NullPointerException,因此必须在 switch 外部进行 null 检查:

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");
}
}
java

switch 仅支持少数引用类型时,这是合理的。然而,如果 switch 允许任意类型的匹配表达式,并且 case 标签可以具有类型模式,那么单独的 null 检查就显得像是一个武断的区别,并且会引发不必要的样板代码和出错机会。最好将 null 检查集成到 switch 中:

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");
}
}
java

当选择器表达式的值为 null 时,switch 的行为始终由其 case 标签决定。如果有 case null(或全类型模式;参见下方 4a),switch 将执行与该标签关联的代码;如果没有 case nullswitch 将抛出 NullPointerException,与之前的行为一致。(为了与当前 switch 的语义保持向后兼容,default 标签不会匹配 null 选择器。)

我们可能希望以与另一个 case 标签相同的方式来处理 null。例如,在以下代码中,case null, String s 将匹配 null 值以及所有的 String 值:

static void testStringOrNull(Object o) {
switch (o) {
case null, String s -> System.out.println("String: " + s);
}
}
java

switch 中优化模式

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");
}
}
java

这段代码的意图是为大三角形(面积超过 100)提供一个特殊处理情况,并为其他所有情况(包括小三角形)提供一个默认处理情况。然而,我们无法通过单个模式直接表达这一点。首先,我们必须编写一个匹配所有三角形的 case 标签,然后将对三角形面积的测试放在相应的语句组中,这显得有些不太直观。接着,当三角形的面积小于 100 时,我们不得不使用贯穿(fall-through)来获得正确的行为。(注意 break;if 块中的精心放置位置。)

这里的问题在于,使用单一模式来区分不同情况的方式无法扩展到单一条件之外。我们需要某种方法来表达对模式的细化。一种可能的做法是允许 case 标签进行细化;这种细化在其他编程语言中被称为保护条件(guard)。例如,我们可以引入一个新的关键字 where,让它出现在 case 标签的末尾,并在其后跟随一个布尔表达式,例如:case Triangle t where t.calculateArea() > 100

然而,还有另一种方法:我们不必扩展 case 标签的功能,而是可以直接扩展模式语言本身。我们可以添加一种称为 guarded pattern(受保护模式)的新模式,写作 p && b,它允许通过任意布尔表达式 b 来细化模式 p

通过这种方法,我们可以重新审视 testTriangle 代码,以直接表达大三角形的特殊情况。这样就消除了在 switch 语句中使用贯穿(fall-through),从而意味着我们可以享受简洁的箭头风格(->)规则:

static void testTriangle(Shape s) {
switch (s) {
case Triangle t && (t.calculateArea() > 100) ->
System.out.println("Large triangle");
default ->
System.out.println("A shape, possibly a small triangle");
}
}
java

如果 s 的值首先匹配类型模式 Triangle t,并且表达式 t.calculateArea() > 100 的计算结果为 true,那么它就符合模式 Triangle t && (t.calculateArea() > 100)

使用 switch 可以在应用需求变更时方便地理解和修改 case 标签。例如,我们可能希望将三角形从默认路径中分离出来;这可以通过同时使用细化模式和非细化模式来实现:

static void testTriangle(Shape s) {
switch (s) {
case Triangle t && (t.calculateArea() > 100) ->
System.out.println("Large triangle");
case Triangle t ->
System.out.println("Small triangle");
default ->
System.out.println("Non-triangle");
}
}
java

描述

我们通过两种方式增强了 switch 语句和表达式:

  • 扩展 case 标签以包含模式而不仅仅是常量,并且

  • 引入两种新的模式:受保护的模式带括号的模式

switch 标签中的模式

该提案的核心是引入一个新的 case p switch 标签,其中 p 是一个模式。switch 的本质保持不变:选择器表达式的值会与 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();
};
java

case 标签可以具有模式时,存在四个主要设计问题:

  1. 增强的类型检查
  2. switch 表达式和语句的完备性
  3. 模式变量声明的作用域
  4. 处理 null

1. 增强的类型检查

1a. 选择器表达式类型

switch 中支持模式匹配意味着我们可以放宽当前对选择器表达式类型的限制。目前,普通 switch 的选择器表达式类型必须是以下之一:整数基本类型(charbyteshortint)、对应的基本类型的包装类(CharacterByteShortInteger)、String 或枚举类型。我们对此进行了扩展,现在要求选择器表达式的类型可以是任何整数基本类型或任何引用类型。

例如,在下面的模式 switch 中,选择器表达式 o 会与涉及类类型、枚举类型、记录类型和数组类型(以及一个 nullcase 标签和一个 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 with " + c.values().length + " values");
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");
}
}
java

switch 块中的每个 case 标签必须与选择器表达式兼容。对于带有模式的 case 标签(称为 模式标签),我们使用现有的 表达式与模式的兼容性 概念 (JLS §14.30.1)。

1b. 模式标签的主导性

选择器表达式有可能在 switch 块中匹配多个标签。考虑以下有问题的示例:

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;
}
}
}
java

第一个模式标签 case CharSequence cs 支配 第二个模式标签 case String s,因为每个匹配模式 String s 的值也匹配模式 CharSequence cs,但反之则不成立。这是因为第二个模式的类型 String 是第一个模式的类型 CharSequence 的子类型。

形如 case p 的模式标签中,如果 p 是选择器表达式的类型的全模式,则它主导了标签 case null。这是因为全模式匹配所有值,包括 null

形式为 case p 的模式标签支配形式为 case p && e 的模式标签,即,其中的模式是原始模式的受保护版本。例如,模式标签 case String s 支配模式标签 case String s && s.length() > 0,因为每个匹配受保护模式 String s && s.length() > 0 的值也匹配模式 String s

模式标签可以支配常量标签。例如,模式标签 case Integer i 支配常量标签 case 42,而当 A 是枚举类类型 E 的枚举常量时,模式标签 case E e 支配常量标签 case A。如果受保护的模式标签所包含的标签能够支配常量标签,则该受保护的模式标签也能支配常量标签;我们不会检查保护表达式,因为这在一般情况下是不可判定的。因此,受保护的模式标签 case String s && s.length()>1 如预期般支配常量标签 case "hello";但 case Integer i && i <> 0 同样支配标签 case 0。这就导致了 case 标签的一种简单且可读的排序方式:常量标签应出现在受保护的模式标签之前,而受保护的模式标签应出现在非受保护的类型模式标签之前:

switch(o) {
case -1, 1 -> ... // Special cases
case Integer i && i > 0 -> ... // Positive integer cases
case Integer i -> ... // All the remaining integers
default ->
}
java

编译器会检查所有标签。如果 switch 块中的某个标签被该 switch 块中较早出现的标签所支配(覆盖),则会产生编译时错误。这种支配要求确保了,如果一个 switch 块仅包含类型模式的 case 标签,它们将按照子类型顺序出现。

(支配的概念类似于 try 语句中对 catch 子句的条件要求,即如果捕获异常类 Ecatch 子句前面存在一个能够捕获 EE 的超类的 catch 子句,则会出错(参见 JLS §11.2.3)。从逻辑上讲,前面的 catch 子句支配了后面的 catch 子句。)

如果一个 switch 块中存在多个匹配所有情况的 switch 标签,同样会引发编译时错误。两个 匹配所有 的标签分别是 default 和全类型模式(详见 4a,下文)。

2. switch 表达式和语句的完备性

switch 表达式要求在 switch 块中处理选择器表达式的所有可能值;换句话说,它是穷尽的。这保证了成功计算一个 switch 表达式后总会产生一个值。对于普通的 switch 表达式,这是通过对 switch 块施加一组相当直接的额外条件来强制执行的。对于模式 switch 表达式,我们通过定义 switch 块中 switch 标签的类型覆盖概念来实现这一点。然后将 switch 块中所有 switch 标签的类型覆盖合并起来,以确定该 switch 块是否穷尽了选择器表达式的所有可能性。

考虑以下(错误的)模式 switch 表达式:

static int coverage(Object o) {
return switch (o) { // Error - not exhaustive
case String s -> s.length();
};
}
java

switch 块只有一个 case 标签,即 case String s。这将匹配选择器表达式中类型为 String 子类型的所有值。因此,我们说这个 switch 标签的类型覆盖范围是 String 的所有子类型。此模式 switch 表达式并不详尽,因为其 switch 块的类型覆盖范围(String 的所有子类型)并未包含选择器表达式的类型(Object)。

请看这个(仍然有错误的)例子:

static int coverage(Object o) {
return switch (o) { // Error - not exhaustive
case String s -> s.length();
case Integer i -> i;
};
}
java

switch 块的类型覆盖范围是其两个 switch 标签覆盖范围的并集。换句话说,类型覆盖范围是 String 的所有子类型和 Integer 的所有子类型的集合。但是,再次强调,类型覆盖范围仍然不包括选择器表达式的类型,因此这种模式的 switch 表达式也不是穷尽的,并会导致编译时错误。

default 标签的类型覆盖了所有类型,所以这个例子(终于!)是合法的:

static int coverage(Object o) {
return switch (o) {
case String s -> s.length();
case Integer i -> i;
default -> 0;
};
}
java

如果选择器表达式的类型是一个密封类(JEP 409),那么类型覆盖率检查可以考虑密封类的 permits 子句,以确定 switch 块是否详尽无遗。这有时可以消除对 default 子句的需求。请看以下示例,其中有一个 sealed 接口 S,它允许三个子类 ABC

sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {} // Implicitly final

static int testSealedExhaustive(S s) {
return switch (s) {
case A a -> 1;
case B b -> 2;
case C c -> 3;
};
}
java

编译器可以确定 switch 块的类型覆盖范围为类型 ABC。由于选择器表达式的类型 S 是一个密封接口,其允许的子类正好是 ABC,因此这个 switch 块是穷尽的。结果就是,不需要 default 标签。

当允许的直接子类仅实现(泛型)sealed 超类的实例化时,需要一些额外的注意。例如:

sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}

static int testGenericSealedExhaustive(I<Integer> i) {
return switch (i) {
// Exhaustive as no A case possible!
case B<Integer> bi -> 42;
}
}
java

I 唯一允许的子类是 AB,但编译器可以检测到 switch 块只需要覆盖类 B 即可穷尽,因为选择器表达式的类型是 I<Integer>

为了防止不兼容的独立编译,编译器会自动添加一个 default 标签,其代码会抛出 IncompatibleClassChangeError 异常。只有当 sealed 接口被修改且 switch 代码未重新编译时,才会执行到该标签。实际上,编译器会自动强化你的代码。

(对模式 switch 表达式要求详尽无遗的条件类似于对选择器表达式为枚举类的 switch 表达式的处理,如果枚举类的每个常量都有一个对应的分支,则不需要 default 分支。)

让编译器验证 switch 表达式是否详尽无遗的实用性非常强。我们不仅将这种检查保留给 switch 表达式,还将其扩展到 switch 语句中。为了确保向后兼容性,所有现有的 switch 语句都将保持不变地编译。但如果某个 switch 语句使用了本 JEP 中详细说明的任何新特性,那么编译器将会检查其是否详尽无遗。

更准确地说,如果 switch 语句使用了模式或 null 标签,或者其选择表达式不是以下遗留类型之一(charbyteshortintCharacterByteShortIntegerString 或枚举类型),则必须满足完备性要求。

这意味着现在 switch 表达式和 switch 语句都可以受益于更严格的类型检查。例如:

sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {} // Implicitly final

static void switchStatementExhaustive(S s) {
switch (s) { // Error - not exhaustive; missing clause for permitted class B!
case A a :
System.out.println("A");
break;
case C c :
System.out.println("C");
break;
};
}
java

使大多数 switch 语句变得详尽无遗,只需在 switch 块的末尾添加一个简单的 default 子句即可。这会使代码更加清晰且易于验证。例如,以下 switch 语句并不详尽,且存在错误:

Object o = ...
switch (o) { // Error - not exhaustive!
case String s:
System.out.println(s);
break;
case Integer i:
System.out.println("Integer");
break;
}
java

它可以被详尽地制作:

Object o = ...
switch (o) {
case String s:
System.out.println(s);
break;
case Integer i:
System.out.println("Integer");
break;
default: // Now exhaustive!
break;
}
java

(未来版本的 Java 语言编译器可能会对非穷尽的旧式 switch 语句发出警告。)

3. 模式变量声明的范围

模式变量JEP 394)是由模式声明的局部变量。模式变量声明的特殊之处在于其作用域是流敏感型的。以下面的例子回顾一下,其中类型模式 String s 声明了模式变量 s

static void test(Object o) {
if ((o instanceof String s) && s.length() > 3) {
System.out.println(s);
} else {
System.out.println("Not a string");
}
}
java

&& 表达式的右侧操作数以及“then”块中,s 的声明是有效的。然而,它在“else”块中无效;为了使控制流转移到“else”块,模式匹配必须失败,在这种情况下,模式变量将不会被初始化。

我们将这种对模式变量声明的流敏感作用域概念扩展到包含出现在 case 标签中的模式声明,并引入两条新规则:

  1. switch 规则的 case 标签中出现的模式变量声明的作用域包括出现在箭头右侧的表达式、代码块或 throw 语句。

  2. 在带有标签的语句组的 switchcase 标签中出现的模式变量声明的作用域包含该语句组的代码块语句。不应该能够贯穿声明了模式变量的 case 标签。

这个示例展示了第一条规则的实际应用:

static void test(Object o) {
switch (o) {
case Character c -> {
if (c.charValue() == 7) {
System.out.println("Ding!");
}
System.out.println("Character");
}
case Integer i ->
throw new IllegalStateException("Invalid Integer argument of value " + i.intValue());
default -> {
break;
}
}
}
java

模式变量 c 的声明范围是第一个箭头右侧的块。

模式变量 i 的声明范围是第二个箭头右侧的 throw 语句。

第二条规则更为复杂。让我们先来看一个例子,其中只有一个 case 标签对应一个 switch 标记语句组:

static void test(Object o) {
switch (o) {
case Character c:
if (c.charValue() == 7) {
System.out.print("Ding ");
}
if (c.charValue() == 9) {
System.out.print("Tab ");
}
System.out.println("character");
default:
System.out.println();
}
}
java

模式变量 c 的声明范围包括语句组中的所有语句,即两个 if 语句和 println 语句。该范围不包括 default 语句组中的语句,即使第一个语句组的执行可以贯穿 default 开关标签并执行这些语句。

通过声明模式变量的 case 标签跌落的可能性必须作为编译时错误排除。考虑以下错误示例:

static void test(Object o) {
switch (o) {
case Character c:
if (c.charValue() == 7) {
System.out.print("Ding ");
}
if (c.charValue() == 9) {
System.out.print("Tab ");
}
System.out.println("character");
case Integer i: // Compile-time error
System.out.println("An integer " + i);
default:
break;
}
}
java

如果允许这样做,并且选择器表达式 o 的值为 Character,那么在执行 switch 块时可能会跳过第二个语句组(在 case Integer i: 之后),此时模式变量 i 可能尚未初始化。因此,允许执行跳过声明了模式变量的 case 标签属于编译时错误。

这就是为什么 case Character c: case Integer i: ... 不被允许的原因。类似的推理也适用于禁止在 case 标签中使用多个模式:无论是 case Character c, Integer i: ... 还是 case Character c, Integer i -> ... 都不被允许。如果允许这样的 case 标签,那么在冒号或箭头之后,ci 都会在作用域内,但它们之中只有一个会被初始化,具体取决于 o 的值是 Character 还是 Integer

另一方面,像这个例子展示的那样,通过一个未声明模式变量的标签进行跳转是安全的:

void test(Object o) {
switch (o) {
case String s:
System.out.println("A string");
default:
System.out.println("Done");
}
}
java

4. 处理 null

4a. 匹配 null

传统上,如果选择器表达式计算结果为 nullswitch 会抛出 NullPointerException。这种行为是广为人知的,我们并不建议对任何现有的 switch 代码进行更改。

然而,鉴于模式匹配和 null 值存在合理且不引发异常的语义,这为我们提供了一个机会,可以在保持与现有 switch 语义兼容的同时,让模式 switchnull 更加友好。

首先,我们为 case 引入一个新的 null 标签,当选择器表达式的值为 null 时匹配。

其次,我们观察到,如果一个对于选择器表达式的类型来说是完全的模式出现在模式 case 标签中,那么当选择器表达式的值为 null 时,该标签也会匹配。(如果类型为 U 的类型模式 p 对于类型 T完全的,则 TU 的子类型。例如,类型模式 Object o 对于类型 String 是完全的。)

我们取消了“如果选择器表达式的值为 nullswitch 会立即抛出 NullPointerException”的笼统规则。相反,我们会检查 case 标签以确定 switch 的行为:

  • 如果选择器表达式计算结果为 null,则任何 null 的 case 标签或全模式 case 标签被认为匹配。如果 switch 块中没有与此类标签关联,则 switch 会抛出 NullPointerException,与之前相同。

  • 如果选择器表达式计算结果为非 null 值,则正常选择匹配的 case 标签。如果没有 case 标签匹配,则任何匹配所有内容的标签将被视为匹配。

例如,给定以下声明,求值 test(null) 会打印 null! 而不是抛出 NullPointerException

static void test(Object o) {
switch (o) {
case null -> System.out.println("null!");
case String s -> System.out.println("String");
default -> System.out.println("Something else");
}
}
java

这种围绕 null 的新行为就好像编译器自动丰富了 switch 块,为其添加了一个抛出 NullPointerExceptioncase null。换句话说,以下代码:

static void test(Object o) {
switch (o) {
case String s -> System.out.println("String: " + s);
case Integer i -> System.out.println("Integer");
default -> System.out.println("default");
}
}
java

等价于:

static void test(Object o) {
switch (o) {
case null -> throw new NullPointerException();
case String s -> System.out.println("String: "+s);
case Integer i -> System.out.println("Integer");
default -> System.out.println("default");
}
}
java

在这两个示例中,评估 test(null) 都将导致抛出 NullPointerException

我们保留了现有 switch 结构的直观理解,即对 null 进行切换是一种异常操作。在模式 switch 中的不同之处在于,你有一个机制可以直接在 switch 内部处理这种情况,而不是外部。如果你选择不在 switch 块中包含匹配 null 的 case 标签,那么对 null 值进行切换时将像以前一样抛出 NullPointerException

4b. 由 null 标签产生的新标签形式

Java 16 中的 switch 块支持两种风格:一种基于带标签的语句组(: 形式),其中允许贯穿;另一种基于单结果形式(-> 形式),其中不允许贯穿。在前一种风格中,多个标签通常写作 case l1: case l2:,而在后一种风格中,多个标签写作 case l1, l2 ->

支持 null 标签意味着可以在 : 形式中表达许多特殊情况。例如:

Object o = ...
switch(o) {
case null: case String s:
System.out.println("String, including null");
break;
...
}
java

开发者有理由认为 :-> 形式具有相同的表达能力,并且如果前者支持 case A: case B:,那么后者应该支持 case A, B ->。因此,前面的例子表明我们应该支持 case null, String s -> 标签,如下所示:

Object o = ...
switch(o) {
case null, String s -> System.out.println("String, including null");
...
}
java

o 为 null 引用或者是 String 类型时,o 的值与这个标签匹配。在这两种情况下,模式变量 s 都会用 o 的值进行初始化。

(反过来的形式,case String s, null 也是允许的,并且行为相同。)

null 情况与 default 标签结合使用也是有意义的,并且并不罕见,即:

Object o = ...
switch(o) {
...
case null: default:
System.out.println("The rest (including null)");
}
java

同样,这应该在 -> 形式中得到支持。为此,我们引入了一个新的 default 情况标签:

Object o = ...
switch(o) {
...
case null, default ->
System.out.println("The rest (including null)");
}
java

如果 o 的值是空引用值,或者没有其他标签匹配,则 o 的值与此标签匹配。

带保护和括号的模式

成功匹配模式后,我们通常会进一步测试匹配结果。这可能会导致代码变得冗长,例如:

static void test(Object o) {
switch (o) {
case String s:
if (s.length() == 1) { ... }
else { ... }
break;
...
}
}
java

所期望的测试 —— 即 o 是一个长度为 1 的 String —— 不幸地被分散在 case 标签和随后的 if 语句之间。如果模式 switch 支持在 case 标签中结合模式与布尔表达式,我们就可以提高可读性。

我们并没有添加另一个特殊的 case 标签,而是通过引入受保护的模式来增强模式语言,其写法为 p && e。这使得上面的代码可以被重写,从而将所有的条件逻辑提升到 case 标签中:

static void test(Object o) {
switch (o) {
case String s && (s.length() == 1) -> ...
case String s -> ...
...
}
}
java

如果 o 既是 String 而且长度为 1,则匹配第一种情况。如果 o 是具有其他长度的 String,则匹配第二种情况。

有时,我们需要给模式加括号以避免解析歧义。因此,我们扩展了模式语言以支持写为 (p) 的带括号模式,其中 p 是一个模式。

更准确地说,我们更改了模式的语法。假设添加了 JEP 405 的记录模式和数组模式,模式的语法将变为:

Pattern:
PrimaryPattern
GuardedPattern

GuardedPattern:
PrimaryPattern && ConditionalAndExpression

PrimaryPattern:
TypePattern
RecordPattern
ArrayPattern
( Pattern )

一个受保护的模式的形式为 p && e,其中 p 是一个模式,e 是一个布尔表达式。在受保护的模式中,任何在子表达式中使用但未声明的局部变量、形式参数或异常参数必须是 final 或实际上是 final 的。

受保护的模式 p && e 引入了模式 p 和表达式 e 所引入的模式变量的并集。p 中任何模式变量声明的作用域都包括表达式 e。这允许像 String s && (s.length() > 1) 这样的模式,它匹配一个可以转换为 String 的值,且该字符串的长度大于一。

如果一个值首先匹配模式 p,其次表达式 e 的计算结果为 true,那么该值就匹配受保护的模式 p && e。如果该值不匹配 p,则不会尝试计算表达式 e

带括号的模式 的形式为 (p),其中 p 是一个模式。带括号的模式 (p) 引入了由子模式 p 引入的模式变量。如果一个值匹配模式 p,则它匹配带括号的模式 (p)

我们还更改了 instanceof 表达式的语法为:

InstanceofExpression:
RelationalExpression instanceof ReferenceType
RelationalExpression instanceof PrimaryPattern
java

这一改动,以及受保护模式的语法规则中的非终结符 ConditionalAndExpression,确保了例如表达式 e instanceof String s && s.length() > 1 继续明确地解析为表达式 (e instanceof String s) && (s.length() > 1)。如果尾随的 && 意在成为受保护模式的一部分,则整个模式应该被括起来,例如,e instanceof (String s && s.length() > 1)

在受保护模式的语法规则中使用非终结符 ConditionalAndExpression 还消除了关于带有受保护模式的 case 标签的另一个潜在歧义。例如:

boolean b = true;
switch (o) {
case String s && b -> s -> s;
}
java

如果允许受保护模式的保护表达式为任意表达式,那么就会出现歧义,即第一个出现的 -> 是属于 lambda 表达式的一部分,还是属于 switch 规则(其主体是一个 lambda 表达式)的一部分。由于 lambda 表达式永远不可能是有效的布尔表达式,因此限制保护表达式的语法规则是安全的。

未来工作

  • 目前,模式 switch 不支持原始类型 booleanfloatdouble。它们的实用性似乎很小,但可以添加对这些类型的支持。

  • 我们预计,在未来,通用类将能够声明解构模式以指定它们如何被匹配。这样的解构模式可以与模式 switch 一起使用,从而生成非常简洁的代码。例如,如果我们有一个 Expr 的层次结构,其中包含 IntExpr(包含单个 int)、AddExprMulExpr(包含两个 Expr)以及 NegExpr(包含单个 Expr)的子类型,那么我们可以在一个步骤中匹配 Expr 并针对特定的子类型进行操作:

    int eval(Expr n) {
    return switch(n) {
    case IntExpr(int i) -> i;
    case NegExpr(Expr n) -> -eval(n);
    case AddExpr(Expr left, Expr right) -> eval(left) + eval(right);
    case MulExpr(Expr left, Expr right) -> eval(left) * eval(right);
    default -> throw new IllegalStateException();
    };
    }
    java

    如果没有这种模式匹配,表达这种特定的多态计算就需要使用繁琐的 访问者模式。模式匹配通常更加透明和直接。

  • 添加 AND 和 OR 模式也可能很有用,以便为带有模式的 case 标签提供更多表达能力。

替代方案

  • 与其支持模式 switch,我们也可以定义一个 类型 switch,它仅支持根据选择器表达式的类型进行切换。此功能在规范和实现上较为简单,但表达能力却差得多。

  • 对于带保护的模式,还有许多其他的语法选项,例如 p where ep when ep if e,甚至是 p &&& e

  • 带保护模式的一种替代方案是直接将 保护条件 支持为一种特殊的 case 标签形式:

    SwitchLabel:
    case Pattern [ when Expression ]
    ...

    case 标签中支持保护条件需要引入 when 作为一个新的上下文关键字,而带保护的模式则不需要新的上下文关键字或运算符。带保护的模式提供了更大的灵活性,因为带保护的模式可以出现在其应用位置附近,而不是在 switch 标签的末尾。

依赖

此 JEP 建立在 instanceof 的模式匹配(JEP 394)以及 switch 表达式提供的增强功能(JEP 361)之上。当 JEP 405(记录模式和数组模式)出现时,最终的实现可能会利用动态常量(JEP 309)。