跳到主要内容

JEP 427:开关的模式匹配(第三次预览)

概括

switch通过表达式和语句的模式匹配增强 Java 编程语言。扩展模式匹配允许switch针对多个模式测试表达式,每个模式都有一个特定的操作,以便可以简洁、安全地表达复杂的面向数据的查询。这是预览语言功能

历史

模式匹配由JEP 406switch提议作为预览功能并在JDK 17中提供,并由JEP 420提议作为第二个预览并在JDK 18中提供。此 JEP 提出了第三个预览版,并根据持续的经验和反馈进行了进一步的改进。

自第二次预览以来的主要变化是:

  • 受保护的模式被替换为whenswitch 块中的子句。

  • 当选择器表达式的值为 时,模式切换的运行时语义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 nullswitch执行与该标签关联的代码;如果没有 a case null,则switch抛出NullPointerException,就像以前一样。 (为了保持与当前语义的向后兼容性switchdefault标签与选择器不匹配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 来获得正确的行为。(请注意语句breakif块内的小心放置。)

这里的问题是,使用单一模式来区分情况并不能超出单一条件——我们需要某种方法来表达对模式的细化。一种方法是引入书面_的受保护模式_p && b,允许通过任意布尔表达式对模式p进行细化b

我们在此 JEP 的前身中实现了受保护的模式。根据经验和反馈,我们建议改为允许whenswitch 块中的子句指定模式标签的防护,例如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 的oa String)被分割在模式case标签和以下if语句之间。

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

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

第一个子句匹配 if ois aString _且_长度为 1。第二个子句匹配 if ois aString任意长度。

只有图案标签才能有防护。例如,编写带有常量case和保护的标签是无效的,例如case "Hello" when RandomBooleanExpression()

有时我们需要将模式括起来以提高可读性。因此,我们扩展了模式语言以支持编写_的括号模式_(p),其中p是模式。带括号的模式(p)引入了由子模式引入的模式变量p(p)如果值与模式匹配,则该值与带括号的模式匹配p

支持以下模式时需要考虑四个主要的语言设计领域switch

  1. 增强类型检查
  2. switch表达式和陈述的详尽性
  3. 模式变量声明的范围
  4. 处理null

1. 增强类型检查

1a.选择器表达式输入

支持 in 模式switch意味着我们可以放宽当前对选择器表达式类型的限制。目前,法线的选择器表达式的类型switch必须是整型原始类型(不包括long)、相应的装箱形式(即 、CharacterByteShortIntegerStringenum类型。我们对此进行了扩展,并要求选择器表达式的类型是整型原始类型(不包括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 scase 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属于模式标签类型StringCharSequence因为它首先出现在开关块中。模式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必须与模式标签匹配case String s

仅当前者的模式支配后者的模式_并且_其保护是 value 的常量表达式时,受保护的模式标签才支配另一个模式标签(受保护或不受保护) true。例如,受保护的模式标签case String s when true支配模式标签case String s。我们不再进一步分析保护表达式,以便更精确地确定哪些值与模式标签匹配(这个问题通常是无法判定的)。

模式标签可以支配常量标签。例如,模式标签case Integer i支配常量标签,当是枚举类类型的枚举常量时case 42,模式标签case E e支配常量标签。如果没有子句的相同模式标签支配常量标签,则受保护的模式标签支配常量标签。换句话说,我们不检查守卫,因为这通常是不可判定的。例如,正如预期的那样,模式标签支配着常量标签;但霸占了标签。case A``A``E``when``case String s when s.length() > 1``case "hello"``case Integer i when i != 0``case 0

所有这些都表明了一种简单、可预测且可读的 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
}

编译器检查所​​有标签。如果 switch 块中的标签被该 switch 块中较早的标签所控制,则这是一个编译时错误。此主导要求确保如果 switch 块仅包含类型模式大小写标签,它们将按子类型顺序出现。

(支配的概念类似于catch语句子句的条件,如果捕获异常类的子句前面有一个可以捕获异常类的子句或( JLS §11.2.3 )的超类,try则这是一个错误。逻辑上,前面的子句支配后面的子句。)catch``E``catch``E``E``catch``catch

switch如果表达式或switch语句的 switch 块具有多个全匹配 switch 标签,这也是一种编译时错误。全部匹配标签是default模式标签,其中模式无条件匹配选择器表达式。例如,类型模式String s无条件匹配 type 的选择器表达式String,而类型模式Object o无条件匹配任何引用类型的选择器表达式。

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

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

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

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

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

标签的类型覆盖范围default是所有类型,因此这个示例(终于!)合法:

static int coverage(Object o) {
return switch (o) {
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 块的类型覆盖是类型A, B, 和C。由于选择器表达式 的类型S是一个密封接口,其允许的子类正是ABC,因此该开关块是详尽的。因此,不需要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;
}
}

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

为了防止不兼容的单独编译,在这种情况下是一个switch密封类,其中 switch 块是详尽的并且没有匹配所有情况,编译器会自动添加一个default标签,其代码会抛出IncompatibleClassChangeError.只有当sealed接口改变并且switch代码没有重新编译时才会到达这个标签。实际上,编译器会为您强化代码。

这种详尽的条件适用于模式switch表达式和模式switch 语句。为了确保向后兼容性,所有现有switch语句将按原样进行编译。但是,如果switch语句使用此 JEP 中详细介绍的任何switch增强功能,那么编译器将检查它是否详尽。 (Java 语言的未来编译器可能会针对switch不详尽的遗留语句发出警告。)

switch更准确地说,任何使用模式或null标签或其选择器表达式不是旧类型(charbyteshortintCharacterByteShort、 、IntegerString或枚举类型)之一的语句都需要详尽。例如:

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

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

它可以变得详尽无遗:

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

记录模式JEP 405 )使详尽性的概念变得更加复杂,因为这些模式支持在其中嵌套其他模式。因此,详尽性的概念必须反映这种潜在的递归结构。

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

的声明s位于表达式右侧操作数&&以及“then”块的范围内。但是,它不在“else”块的范围内;为了将控制转移到“else”块,模式匹配必须失败,在这种情况下,模式变量将不会被初始化。

我们扩展了模式变量声明的这种流敏感的范围概念,以包含使用case三个新规则的标签中出现的模式声明:

  1. 出现在 switch 标签中的模式变量声明的范围包括when该标签的任何子句。

  2. case规则标签中出现的模式变量声明的范围包括出现在箭头右侧的switch表达式、块或语句。throw

  3. case出现在带标签语句组的标签中的模式变量声明的范围switch包括该语句组的块语句。case禁止跌倒声明模式变量的标签。

此示例显示了第一条正在运行的规则:

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

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

此变体显示了第二条规则的作用:

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: "
+ i.intValue());
default -> {
break;
}
}
}

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

第三条规则比较复杂。让我们首先考虑一个示例,其中带标签的语句组只有一个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();
}
}

模式变量的声明范围c包括语句组的所有语句,即两条if语句和一条println语句。该范围不包括该default语句组的语句,尽管第一个语句组的执行可以通过defaultswitch 标签并执行这些语句。

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

如果允许这样做并且选择器表达式的值为oa Character,则 switch 块的执行可能会通过第二个语句组( after case Integer i:),其中模式变量i不会被初始化。因此,允许执行通过case声明模式变量的标签是一个编译时错误。

case Character c: case Integer i: ...这就是为什么不允许使用由多个模式标签组成的开关标签,例如。类似的推理也适用于禁止在单个case标签内使用多种模式:既不case Character c, Integer i: ...case Character c, Integer i -> ...不允许。如果case允许这样的标签,则ci都将在冒号或箭头之后的范围内,但根据 的值是oaCharacter还是 an ,只有其中一个会被初始化Integer

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

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

4. 处理null

4a.匹配null

传统上,如果选择器表达式的计算结果为 ,则switch抛出a 。这是众所周知的行为,我们不建议对任何现有代码进行更改。NullPointerException``null``switch

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

首先,我们nullcase.然后,我们解除一揽子规则,如果选择器表达式的值为 ,则aswitch立即抛出。相反,我们检查标签来确定 a 的行为:NullPointerException``null``case``switch

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

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

例如,给出下面的声明,评估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");
}
}

这种新行为就好像编译器自动用其主体抛出null异常来丰富 switch 块。换句话说,这段代码:case null``NullPointerException

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

相当于:

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

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

我们保留了现有结构的直觉switch,即执行切换null是一件特殊的事情。模式的区别switch在于,您有一种机制可以在内部switch而不是外部直接处理这种情况。如果您null在开关块中看到标签,则该标签将匹配一个null值。如果您在 switch 块中没有看到null标签,则切换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;
...
}

开发人员合理地期望:->形式具有同等的表达能力,并且如果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");
...
}

o当 的值是空引用或者是 时,它的值与该标签匹配String。在这两种情况下,模式变量s都使用 的值进行初始化o。 (相反的形式case String s, null也是允许的,并且行为相同。)

null将案例与标签结合起来也是有意义的,而且并不罕见,default

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

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

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

o如果 的值是空参考值,或者没有其他标签匹配,则的值与该标签匹配。

未来的工作

  • 目前,模式switch不支持基本类型booleanlongfloatdouble。它们的效用似乎很小,但可以添加对它们的支持。

  • 我们期望,将来,通用​​类将能够声明解构模式来指定它们如何匹配。这种解构模式可以与模式一起使用switch来生成非常简洁的代码。例如,如果我们有一个Expr包含子类型 for IntExpr(包含单个int)、AddExprand MulExpr(包含两个Exprs)和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 ) 提供的增强功能。当JEP 405(记录模式)出现时,最终的实现可能会使用动态常量(JEP 309)。