跳到主要内容

JEP 433:switch 模式匹配(第四预览版)

QWen Max 中英对照 JEP 433: Pattern Matching for switch (Fourth Preview)

总结

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

历史

switch 的模式匹配被 JEP 406 提议为预览特性,并在 JDK 17 中交付,后由 JEP 420 提议进行第二次预览并在 JDK 18 中交付,再由 JEP 427 提议进行第三次预览并在 JDK 19 中交付。本 JEP 提议进行第四次预览,以促进其与记录模式(Record Patterns)预览特性(JEP 432)的持续协同演进,并基于持续的经验和反馈进行其他改进。

自第三个预览版以来的主要变化包括:

  • 如果在运行时没有适用的 switch 标签,对 enum 类进行详尽的 switch(即 switch 表达式或模式 switch 语句)现在会抛出 MatchException 而不是 IncompatibleClassChangeError

  • switch 标签的语法更简单了。

  • switch 表达式和语句中,现在支持泛型记录模式的类型参数推断,其他支持模式的结构也同样支持。

目标

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

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

  • 通过要求模式 switch 语句覆盖所有可能的输入值,提高 switch 语句的安全性。

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

动机

在 Java 16 中,JEP 394instanceof 运算符进行了扩展,使其能够接受一个类型模式并执行模式匹配。这一适度的扩展简化了常见的 instanceof-然后-强制转换的习惯用法,使其更加简洁且不易出错:

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

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

我们经常需要将一个变量(例如 obj)与多个选项进行比较。Java 通过 switch 语句支持多路比较,并且从 Java 14 开始支持 switch 表达式(JEP 361),但遗憾的是,switch 的功能非常有限。你只能对少数几种类型的值进行切换 —— 整型基本类型(不包括 long)、它们对应的包装类形式、enum 类型以及 String —— 并且只能针对常量进行精确相等性测试。我们可能希望使用模式来针对多种可能性测试同一个变量,并在每种情况下采取特定的操作,但由于现有的 switch 不支持这一点,最终我们只能使用一连串的 if...else 测试,例如:

static String formatter(Object obj) {
String formatted = "unknown";
if (obj instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (obj instanceof Long l) {
formatted = String.format("long %d", l);
} else if (obj instanceof Double d) {
formatted = String.format("double %f", d);
} else if (obj instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}

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

但是 switch 非常适合用于模式匹配!如果我们将 switch 语句和表达式扩展到适用于任何类型,并允许 case 标签使用模式而不仅仅是常量,那么我们可以更清晰、更可靠地重写上面的代码:

static String formatterPatternSwitch(Object obj) {
return switch (obj) {
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 -> obj.toString();
};
}

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

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

开关与空值

传统上,如果选择器表达式计算结果为 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");
}
}

switch 仅支持少数引用类型时,这是合理的。然而,如果 switch 允许选择器表达式为任意类型,并且 case 标签可以具有类型模式,那么单独的 null 检查就显得像是一个武断的区别,还会引发不必要的模板代码和出错机会。通过允许新的 null case 标签,将 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");
}
}

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

案例提炼

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 时,我们不得不使用贯穿(fall-through)来获得正确的行为。(注意 break 语句在 if 块中的谨慎放置。)

这里的问题在于,使用单一模式来区分不同情况无法扩展到单一条件之外 —— 我们需要某种方式来表达对模式的细化。因此,我们允许在 switch 块中使用 when 子句为模式 case 标签指定保护条件,例如,case Triangle t when t.calculateArea() > 100。我们将这样的 case 标签称为带保护的 case 标签,并将布尔表达式称为保护条件

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

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 标签。例如,我们可能希望将三角形从默认路径中分离出来;这可以通过使用两个 case 子句来实现,一个带有 guard,另一个不带:

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 表达式的选择器表达式的类型范围,并且

  • 允许在 case 标签后添加可选的 when 子句。

为了方便起见,我们还引入了带括号的模式

switch 标签中的模式

我们修改了 switch 块中 switch 标签的语法,具体如下(请参阅 JLS §14.11.1):

SwitchLabel:
case CaseConstant { , CaseConstant }
case null [, default]
case Pattern
default

主要的增强功能是引入了一种新的 case 标签,即 case p,其中 p 是一个模式。switch 的本质保持不变:选择器表达式的值会与 switch 标签进行比较,选择其中一个标签,并执行或计算与该标签关联的代码。现在的区别在于,对于带有模式的 case 标签,所选标签由模式匹配的结果决定,而不是通过相等性测试。例如,在以下代码中,obj 的值与模式 Long l 匹配,并且与标签 case Long l 关联的表达式会被求值:

Object obj = 123L;
String formatted = switch (obj) {
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 -> obj.toString();
};

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

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

期望的测试 —— 即 obj 是长度为 1 的 String —— 不幸地被分割在模式 case 标签和随后的 if 语句之间。

为了解决这个问题,我们通过在模式标签后支持一个可选的保护条件(guard),引入了受保护的模式 case 标签。这允许将上面的代码重写为所有条件逻辑都被提升到 switch 标签中:

static void test(Object obj) {
switch (obj) {
case String s when s.length() == 1 -> ...
case String s -> ...
...
}
}

如果 obj 既是一个 String 长度为 1,那么第一个子句匹配。如果 obj 是任意长度的 String,则第二个子句匹配。

只有模式标签可以带有保护条件。例如,编写带有 case 常量和保护条件的标签是无效的;例如,case "Hello" when RandomBooleanExpression()

有时我们需要为模式添加括号以提高可读性。因此,我们扩展了模式语言以支持写为 (p)带括号的模式,其中 p 是一个模式。带括号的模式 (p) 引入了由子模式 p 引入的模式变量。如果一个值匹配模式 p,那么它也匹配带括号的模式 (p)

switch 中支持模式时,需要考虑五个主要的语言设计领域:

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

1. 增强的类型检查

1a. 选择器表达式类型

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

例如,在下面的模式 switch 中,选择器表达式 obj 会与涉及类类型、enum 类型、记录类型和数组类型的类型模式进行匹配,同时还有一个 nullcase 标签和一个 default

record Point(int i, int j) {}
enum Color { RED, GREEN, BLUE; }

static void typeTester(Object obj) {
switch (obj) {
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 标签意味着,对于选择器表达式的给定值,现在可能会有多个 case 标签适用(以前最多只有一个 case 标签可以适用)。例如,如果选择器表达式的计算结果为 String 类型,那么 case String scase CharSequence cs 这两个 case 标签都适用。

要解决的第一个问题,是确定在此情况下应适用哪个标签。我们并未尝试复杂最佳匹配方法,而是采用了更简单的语义:选择开关块中适用于某个值的第一个 case 标签。

static void first(Object obj) {
switch (obj) {
case String s ->
System.out.println("A string: " + s);
case CharSequence cs ->
System.out.println("A sequence of length " + cs.length());
default -> {
break;
}
}
}

在本例中,如果 obj 的值是 String 类型,则第一个 case 标签将适用;如果它是 CharSequence 类型但不是 String 类型,则第二个模式标签将适用。

但是,如果交换这两个 case 标签的顺序会发生什么呢?

static void error(Object obj) {
switch (obj) {
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;
}
}
}

现在,如果 obj 的值是 String 类型,则应用 CharSequencecase 标签,因为它在 switch 块中首先出现。从某种意义上说,Stringcase 标签是不可达的,因为没有任何选择器表达式的值会导致它被选中。类比于不可达代码,这被视为程序员的错误,并导致编译时错误。

更准确地说,我们说第一个 case 标签 case CharSequence cs 主导 了第二个 case 标签 case String s,因为每个匹配模式 String s 的值同样也匹配模式 CharSequence cs,但反之则不然。这是由于第二个模式的类型 String 是第一个模式的类型 CharSequence 的子类型。

一个未加保护的模式 case 标签主导了一个具有相同模式的受保护模式 case 标签。例如,(未加保护的)模式 case 标签 case String s 主导了受保护的模式 case 标签 case String s when s.length() > 0,因为每个匹配 case 标签 case String s when s.length() > 0 的值也必须匹配 case 标签 case String s

一个带有保护条件的 case 标签仅在其模式支配另一个模式 其保护条件为值为 true 的常量表达式时,才会支配另一个(带或不带保护条件的)模式 case 标签。例如,带有保护条件的模式 case 标签 case String s when true 支配模式 case 标签 case String s。我们不会进一步分析保护表达式以更精确地确定哪些值与模式标签匹配(这是一个通常无法判定的问题)。

模式 case 标签可以支配常量 case 标签。例如,模式 case 标签 case Integer i 支配常量 case 标签 case 42,并且当 A 是枚举类类型 E 的成员时,模式 case 标签 case E e 支配常量 case 标签 case A。如果相同的模式 case 标签在没有保护条件的情况下支配常量 case 标签,那么带保护条件的模式 case 标签也支配该常量 case 标签。换句话说,我们不检查保护条件,因为这在一般情况下是不可判定的。例如,正如预期的那样,模式 case 标签 case String s when s.length() > 1 支配常量 case 标签 case "hello";但 case Integer i when i != 0 支配 case 标签 case 0

所有这些都表明 case 标签应该有一个简单、可预测且易读的顺序:常量 case 标签应出现在带保护模式的 case 标签之前,而带保护模式的 case 标签应出现在不带保护模式的 case 标签之前:

Integer i = ...
switch (i) {
case -1, 1 -> ... // Special cases
case Integer i when i > 0 -> ... // Positive integer cases
case Integer i -> ... // All the remaining integers
}

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

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

如果 switch 表达式或 switch 语句的 switch 块中存在多个匹配所有情况的 switch 标签,这同样会导致编译时错误。匹配所有情况的标签包括 default 和模式 case 标签,其中模式无条件地匹配选择器表达式。例如,类型模式 String s 会无条件地匹配类型为 String 的选择器表达式,而类型模式 Object o 则会无条件地匹配任何引用类型的选择器表达式。

1c. 记录模式中的类型参数推断

如果记录模式命名了一个泛型记录类但未给出类型参数(即,记录模式使用了原始类型),那么类型参数将始终被推断出来。例如:

record MyPair<S,T>(S fst, T snd){};

static void recordInference(MyPair<String, Integer> pair){
switch (pair) {
case MyPair(var f, var s) ->
... // Inferred record Pattern MyPair<String,Integer>(var f, var s)
...
}
}

在所有支持模式的构造中,都支持对记录模式的类型参数进行推断:switch 语句和表达式、instanceof 表达式以及增强的 for 语句。

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

switch 表达式要求在 switch 块中处理选择器表达式的所有可能值;换句话说,它必须是穷尽的。这保证了 switch 表达式成功求值后总会产生一个值。对于普通的 switch 表达式,这是通过对 switch 块的一组相当直接的额外条件来强制执行的。

对于模式 switch 表达式和语句,我们通过定义一个 switch 块中 switch 标签的 类型覆盖 概念来实现这一点。然后将 switch 块中所有 switch 标签的类型覆盖合并起来,以确定 switch 块是否穷尽了选择器表达式的所有可能性。

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

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

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

考虑这个(仍然错误的)例子:

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

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

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

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

如果选择器表达式的类型是一个密封类(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;
};
}

编译器可以确定 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;
}
}

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

这种详尽性条件适用于模式 switch 表达式和模式 switch 语句。为了确保向后兼容,所有现有的 switch 语句都将保持不变地编译。但如果某个 switch 语句使用了此 JEP 中描述的任何 switch 增强功能,则编译器将检查其是否详尽无遗。(Java 语言的未来编译器可能会对那些不详尽的旧版 switch 语句发出警告。)

更准确地说,任何使用模式或 null 标签的 switch 语句,或者其选择表达式不是遗留类型(charbyteshortintCharacterByteShortIntegerString 或枚举类型)之一时,都要求具备完备性。例如:

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

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

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

它可以被彻底地制成:

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

由于 record 模式JEP 432)支持在其内部嵌套其他模式,这使得穷尽性的概念变得更加复杂。因此,穷尽性的概念必须反映这种潜在的递归结构。

3. 模式变量声明的范围

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

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

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

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

  1. 发生在 switch 标签中的模式变量声明的作用域包含该标签的任何 when 子句。

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

  3. 发生在 switch 带标签语句组的 case 标签中的模式变量声明的作用域包含该语句组的代码块语句。禁止通过声明了模式变量的 case 标签直接贯穿。

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

static void test(Object obj) {
switch (obj) {
case Character c
when c.charValue() == 7:
System.out.println("Ding!");
break;
default:
break;
}
}
}

模式变量 c 的声明范围包括 switch 标签的 when 表达式。

此变体展示了第二条规则的实际应用:

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

这里,模式变量 c 的声明范围是第一个箭头右侧的块。模式变量 i 的声明范围是第二个箭头右侧的 throw 语句。

第三条规则更为复杂。让我们首先考虑一个例子,在这个例子中,一个 switch 标签语句组只有一个 case 标签:

static void test(Object obj) {
switch (obj) {
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();
}
}

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

我们禁止通过声明模式变量的 case 标签的可能性。考虑以下错误示例:

static void test(Object obj) {
switch (obj) {
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;
}
}

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

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

另一方面,如本例所示,跳过未声明模式变量的标签是安全的:

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

4. 处理 null

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

然而,鉴于模式匹配和 null 值存在合理且不引发异常的语义,我们有机会使模式 switch 更加对 null 友好,同时仍然与现有的 switch 语义保持兼容。

首先,我们引入了一个新的 null case 标签。然后,我们取消了这样一条通用规则:即如果选择器表达式的值为 nullswitch 语句会立即抛出 NullPointerException。相反,我们会检查 case 标签以确定 switch 的行为:

  • 如果选择器表达式的结果为 null,那么任何 null 的 case 标签都被认为是匹配的。如果 switch 块中没有与此类标签关联的标签,则 switch 会像以前一样抛出 NullPointerException

  • 如果选择器表达式的结果为非 null 值,那么我们像平常一样选择一个匹配的 case 标签。如果没有 case 标签匹配,则任何 default 标签都被认为是匹配的。

例如,对于下面的声明,求值 test(null) 会打印 null! 而不是抛出 NullPointerException

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

这种围绕 null 的新行为就好像编译器自动为 switch 块添加了一个 case null,其主体会抛出 NullPointerException。 换句话说,这段代码:

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

等价于:

static void test(Object obj) {
switch (obj) {
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");
}
}

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

我们保留了现有 switch 结构的直观理解,即对 null 执行 switch 操作是一种异常情况。在模式匹配的 switch 中的区别在于,你有一个机制可以直接在 switch 内部处理这种情况,而不是在外部处理。如果你在 switch 块中看到一个 null 标签,那么这个标签将匹配 null 值。如果你在 switch 块中没有看到 null 标签,那么对 null 值进行 switch 操作时会抛出 NullPointerException 异常,与之前相同。

也希望将 null 情况与 default 结合起来,这是有意义的,而且并不罕见。为此,我们允许 null 情况标签具有一个可选的 default;例如:

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

如果 obj 的值为 null 引用值,或者没有任何其他 case 标签匹配,则 obj 的值与此标签匹配。

如果一个 switch 块同时包含带有 defaultnull case 标签和 default 标签,则会发生编译时错误。

5. 错误

模式匹配可能会突然完成。例如,当将一个值与记录模式进行匹配时,记录的访问器方法可能会突然完成。在这种情况下,模式匹配被定义为通过抛出 MatchException 来突然完成。如果这样的模式作为标签出现在 switch 中,那么 switch 也会通过抛出 MatchException 而突然完成。

如果某个模式受 when 表达式保护,并且计算 when 表达式时突然完成,那么 switch 也会由于相同的原因突然完成。

如果模式 switch 中没有任何标签与选择器表达式的值匹配,那么 switch 会通过抛出 MatchException 异常而突然终止,因为模式 switch 必须是穷尽的。

例如:

record R(int i){
public int i(){ // accessor method for i
return i / 0;
}
}

static void exampleAnR(R r) {
switch(r) {
case R(var i): System.out.println(i);
}
}

调用 exampleAnR(new R(42)) 会导致抛出 MatchException 异常。

相比之下:

static void example(Object obj) {
switch (obj) {
case R r when (r.i / 0 == 1): System.out.println("It's an R!");
default: break;
}
}

调用 example(new R(42)) 会引发 ArithmeticException 异常。

为了与模式 switch 语义保持一致,当在运行时没有任何 switch 标签适用时,基于 enum 类的 switch 表达式现在会抛出 MatchException 而不是 IncompatibleClassChangeError。这是语言的一项小的不兼容变更。

未来工作

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

  • 我们预计,在未来,通用类将能够声明解构模式以指定它们如何进行匹配。这样的解构模式可以与模式 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();
    };
    }

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

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

替代方案

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

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

  • 带保护模式标签的另一种选择是直接支持 带保护的模式 作为一种特殊模式形式,例如 p && e。在之前的预览中进行试验后,由于与布尔表达式存在歧义,我们更倾向于在模式切换中使用 when 子句。

依赖

该 JEP 基于 instanceof 的模式匹配(JEP 394),以及 switch 表达式提供的增强功能(JEP 361)。当 Record Patterns 预览功能(JEP 432)最终确定后,其最终实现可能会利用动态常量(JEP 309)。