JEP 441:开关的模式匹配
概括
switch
通过表达式和语句的模式匹配增强 Java 编程语言。扩展模式匹配允许switch
针对多个模式测试表达式,每个模式都有一个特定的操作,以便可以简洁、安全地表达复杂的面向数据的查询。
历史
此功能最初由JEP 406 (JDK 17)提出,随后由 JEP 420 (JDK 18)、427 (JDK 19) 和433 (JDK 20) 进行完善。它与_记录模式_功能 ( JEP 440 )共同发展,并与之有相当多的交互。该 JEP 建议根据持续的经验和反馈,通过进一步的小改进来最终确定该功能。
除了各种编辑更改外,与之前的 JEP 相比的主要变化是:
-
删除括号内的模式,因为它们没有足够的价值,并且
-
允许限定枚举常量作为表达式和语句
case
中的常量。switch
目标
-
switch
通过允许模式出现在case
标签中来扩展表达式和语句的表现力和适用性。 -
switch
在需要时允许放松历史上的零敌意。 -
switch
通过要求模式switch
语句涵盖所有可能的输入值来提高语句的安全性。 -
确保所有现有
switch
表达式和语句继续编译而不进 行任何更改并以相同的语义执行。
动机
在 Java 16 中,JEP 394扩展了该instanceof
运算符以采用_类型模式_并执行_模式匹配_。这种适度的扩展允许简化熟悉的 instanceof-and-cast 习惯用法,使其更加简洁且不易出错:
// Prior to Java 16
if (obj instanceof String) {
String s = (String)obj;
... use s ...
}
// As of Java 16
if (obj instanceof String s) {
... use s ...
}
在新代码中,如果在运行时 的值是 的实例,则obj
匹配类型模式。如果模式匹配,则表达式为,并且模式变量被初始化为强制转换为的值,然后可以在包含的块中使用该值。String s``obj``String``instanceof``true``s``obj``String
我们经常想要将一个变量与obj
多个替代方案进行比较。 Java 支持switch
语句和switch
表达式(JEP 361 )的多路比较,但不幸的switch
是非常有限。我们只能打开几种类型的值 - 整型原始类型(不包括long
)、它们相应的装箱形式、enum
类型以及String
- 并且我们只能测试与常量的精确相等。我们可能想使用模式来针对多种可能性测试同一变量,对每种可能性采取特定的操作,但由于现有的switch
不支持,我们最终会得到一系列if...else
测试,例如:
// Prior to Java 21
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
表达式,但它远非完美。首先也是最重要的,这种方法可以隐藏编码错误,因为我们使用了过于通用的控制结构。目的是为链formatted
的每个分支分配一些内容if...else
,但没有任何东西可以使编译器识别和强制执行此不变量。如果某个“then”块(也许是很少执行的块)没有分配给formatted
,我们就有一个错误。 (声明formatted
为空白本地至少会在这项工作中利用编译器的明确赋值分析,但开发人员并不总是编写这样的声明。)此外,上述代码不可优化;如果没有编译器英雄,它将具有_O_ ( n ) 时间复杂度,即使底层问题通常是_O_ (1)。
但switch
对于图案搭配来说却是绝配!如果我们扩展switch
语句和表达式以适用于任何类型,并允许使用case
模式标签而不仅仅是常量,那么我们可以更清晰可靠地重写上面的代码:
// As of Java 21
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
很明确:case
如果选择器表达式的值与obj
模式匹配,则应用带有模式的标签。 (为了简洁起见,我们显示了一个switch
表达式,但也可以显示一个switch
语句;switch 块( 包括case
标签)将保持不变。)
这段代码的意图更加清晰,因为我们使用了正确的控制结构:我们是说,“参数obj
最多匹配以下条件之一,找出并评估相应的手臂。”作为奖励,它更加可优化;在这种情况下,我们更有可能能够在_O_ (1) 时间内执行调度。
开关和空值
传统上,如果选择器表达式的计算结果为 ,则switch
语句和表达式会抛出异常,因此必须在 之外进行测试:NullPointerException``null``null``switch
// Prior to Java 21
static void testFooBarOld(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
// As of Java 21
static void testFooBarNew(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
。)
案例细化
与具有常量的标签相比case
,模式case
标签可以应用于许多值。这通常会导致条件代码出现在 switch 规则的右侧。例如,考虑以下代码:
// As of Java 21
static void testStringOld(String response) {
switch (response) {
case null -> { }
case String s -> {
if (s.equalsIgnoreCase("YES"))
System.out.println("You got it");
else if (s.equalsIgnoreCase("NO"))
System.out.println("Shame");
else
System.out.println("Sorry?");
}
}
}
这里的问题是,使用单一模式来区分案例并不能超出单一条件。我们更愿意编写多个模式,但随后我们需要某种方法来表达对模式的细化。因此,我们允许when
switch 块中的子句指定模式case
标签的保护,例如case String s when s.equalsIgnoreCase("YES")
。我们将这样的case
标签称为_受保护_ case
标签,并将布尔表达式称为_守卫_。
通过这种方法,我们可以使用守卫重写上面的代码:
// As of Java 21
static void testStringNew(String response) {
switch (response) {
case null -> { }
case String s
when s.equalsIgnoreCase("YES") -> {
System.out.println("You got it");
}
case String s
when s.equalsIgnoreCase("NO") -> {
System.out.println("Shame");
}
case String s -> {
System.out.println("Sorry?");
}
}
}
这导致了一种更具可读性的编程风格switch
,其中测试的复杂性出现在切换规则的左侧,而满足该测试时应用的逻辑则位于切换规则的右侧。
我们可以使用其他已知常量字符串的额外规则进一步增强此示例:
// As of Java 21
static void testStringEnhanced(String response) {
switch (response) {
case null -> { }
case "y", "Y" -> {
System.out.println("You got it");
}
case "n", "N" -> {
System.out.println("Shame");
}
case String s
when s.equalsIgnoreCase("YES") -> {
System.out.println("You got it");
}
case String s
when s.equalsIgnoreCase("NO") -> {
System.out.println("Shame");
}
case String s -> {
System.out.println("Sorry?");
}
}
}
这些示例展示了case
常量、case
模式和null
标签如何结合起来展示编程的新力量switch
:我们可以将以前与业务逻辑混合的复杂条件逻辑简化为可读的、顺序的开关标签列表,其中业务逻辑位于切换规则。
开关和枚举常量
case
目前,在标签中使用枚举常量受到高度限制:选择器表达式switch
必须是枚举类型,并且标签必须是枚举常量的简单名称。例如:
// Prior to Java 21
public enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES }
static void testforHearts(Suit s) {
switch (s) {
case HEARTS -> System.out.println("It's a heart!");
default -> System.out.println("Some other suit");
}
}
即使添加模式标签后,此约束也会导致不必要的冗长代码。例如:
// As of Java 21
sealed interface CardClassification permits Suit, Tarot {}
public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES }
final class Tarot implements CardClassification {}
static void exhaustiveSwitchWithoutEnumSupport(CardClassification c) {
switch (c) {
case Suit s when s == Suit.CLUBS -> {
System.out.println("It's clubs");
}
case Suit s when s == Suit.DIAMONDS -> {
System.out.println("It's diamonds");
}
case Suit s when s == Suit.HEARTS -> {
System.out.println("It's hearts");
}
case Suit s -> {
System.out.println("It's spades");
}
case Tarot t -> {
System.out.println("It's a tarot");
}
}
}
case
如果我们可以为每个枚举常量提供一个单独的值,而不是使用大量受保护的模式,那么该代码将更具可读性。因此,我们放宽了选择器表达式为枚举类型的要求,并且允许case
常量使用枚举常量的限定名称。这使得上面的代码可以重写为:
// As of Java 21
static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
switch (c) {
case Suit.CLUBS -> {
System.out.println("It's clubs");
}
case Suit.DIAMONDS -> {
System.out.println("It's diamonds");
}
case Suit.HEARTS -> {
System.out.println("It's hearts");
}
case Suit.SPADES -> {
System.out.println("It's spades");
}
case Tarot t -> {
System.out.println("It's a tarot");
}
}
}
现在,我们为每个枚举常量提供了一个直接的情况,而无需使用受保护的类型模式,这些模式以前只是用于解决类型系统的当前约束。
描述
我们switch
通过四种方式增强陈述和表达:
-
改进枚举常量
case
标签, -
扩展
case
标签以包含模式和null
常量, -
switch
扩大语句和表达式的选择器表达式允许的类型范围switch
(以及对 switch 块的详尽性进行更丰富的分析),以及 -
允许可选
when
子句跟随case
标签。
改进的枚举常量case
标签
长期以来,人们一直要求在切换枚举类型时,唯一有效的case
常量是枚举常量。但这是一个强烈的要求,随着新的、更丰富的开关形式的出现,这个要求变得很繁重。
为了保持与现有Java代码的兼容性,当切换枚举类型时,常量case
仍然可以使用被切换的枚举类型的常量的简单名称。
对于新代码,我们扩展了枚举的处理。首先,我们允许枚举常量的限定名称作为case
常量出现。切换枚举类型时可以使用这些限定名称。
其次,当枚举常量之一的名称用作常量时,我们放弃了选择器表达式必须为枚举类型的要求case
。在这种情况下,我们要求名称是限定的,并且其值的赋值与选择器表达式的类型兼容。 (这使枚举常量的处理case
与数值case
常量保持一致。)
例如,允许使用以下两种方法:
// As of Java 21
sealed interface Currency permits Coin {}
enum Coin implements Currency { HEADS, TAILS }
static void goodEnumSwitch1(Currency c) {
switch (c) {
case Coin.HEADS -> { // Qualified name of enum constant as a label
System.out.println("Heads");
}
case Coin.TAILS -> {
System.out.println("Tails");
}
}
}
static void goodEnumSwitch2(Coin c) {
switch (c) {
case HEADS -> {
System.out.println("Heads");
}
case Coin.TAILS -> { // Unnecessary qualification but allowed
System.out.println("Tails");
}
}
}
以下示例是不允许的:
// As of Java 21
static void badEnumSwitch(Currency c) {
switch (c) {
case Coin.HEADS -> {
System.out.println("Heads");
}
case TAILS -> { // Error - TAILS must be qualified
System.out.println("Tails");
}
default -> {
System.out.println("Some currency");
}
}
}
开关标签中的模式
我们修改 switch 块中 switch 标签的语法来读取(比较JLS §14.11.1):
SwitchLabel:
case CaseConstant { , CaseConstant }
case null [, default]
case Pattern [ Guard ]
default
主要增强功能是引入一个新case
标签 ,case p
其中p
是模式。本质switch
没有改变:将选择器表达式的值与开关标签进行比较,选择标签之一,并执行或评估与该标签关联的代码。现在的区别在于,对于case
带有模式的标签,选择的标签是由模式匹配的结果决定的,而不是由相等性测试决定的。例如,在以下代码中, 的值与obj
模式匹配,并且计算Long l
与标签关联的表达式:case Long l
// As of Java 21
static void patternSwitchTest(Object obj) {
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();
};
}
成功的模式匹配后,我们通常会进一步测试匹配的结果。这可能会导致代码变得繁琐,例如:
// As of Java 21
static void testOld(Object obj) {
switch (obj) {
case String s:
if (s.length() == 1) { ... }
else { ... }
break;
...
}
}
不幸的是,所需的测试(即长度为 1 的obj
a String
)被分割在模式case
标签和以下if
语句之间。
为了解决这个问题,我们通过允许可选的_guard_(布尔表达式)跟随模式标签来引入_受保护的模式case
标签_。这允许重写上面的代码,以便将所有条件逻辑提升到 switch 标签中:
// As of Java 21
static void testNew(Object obj) {
switch (obj) {
case String s when s.length() == 1 -> ...
case String s -> ...
...
}
}
第一个子句匹配 if obj
is aString
_且_长度为 1。第二个子句匹配 if obj
is aString
任意长度。
只有图案标签可以有防护装置。例如,写一个带有常量case
和保护的标签是无效的;例如,case "Hello" when callRandomBooleanExpression()
。
支持以下模式时需要考虑五个主要的语言设计领域switch
:
- 增强类型检查
switch
表达式和陈述的详尽性- 模式变量声明的范围
- 处理
null
- 错误
增强类型检查
选择器表达式输入
支持 in 模式switch
意味着我们可以放宽对选择器表达式类型的限制。目前,法线的选择器表达式的类型switch
必须是整型原始类型(不包括long
)、相应的装箱形式(即 、Character
、Byte
或Short
)Integer
、String
或enum
类型。我们对此进行了扩展,并要求选择器表达式的类型是整型原始类型(不包括long
)或任何引用类型。
例如,在以下模式中,switch
选择器表达式与涉及类类型、类型、记录类型和数组类型以及标签和 a 的obj
类型模式匹配:enum``null
case``default
// As of Java 21
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)。
case
标签的主导地位
支持模式case
标签意味着对于选择器表达式的给定值,现在可以应用多个case
标签,而以前最多case
只能应用一个标签。例如,如果选择器表达式的计算结果为 a,String
则case
标签case String s
和case CharSequence cs
都将适用。
要解决的第一个问题是准确决定在这种情况下应应用哪个标签。我们没有尝试复杂的最佳拟合方法,而是采用更简单的语义:case
选择出现在 switch 块中的第一个适用于某个值的标签。
// As of Java 21
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
但是如果我们交换这两个标签的顺序会发生什么?
// As of Java 21
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
标签CharSequence
case
,因为它首先出现在 switch 块中。该String
case
标签是不可访问的,因为选择器表达式没有值会导致它被选择。与无法访问的代码类似,这被视为程序员错误并导致编译时错误。
更准确地说,我们说第一个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
仅当前者的模式支配后者的模式_并且_其保护是 value 的常量表达式时,受保护的模式case
标签才支配另一个模式标签(受保护或不受保护) 。例如,受保护的模式标签支配模式标签。我们不再进一步分析保护表达式,以便更精确地确定哪些值与模式标签匹配——这是一个通常无法判定的问题。case``true``case``case String s when true``case``case String s
模式case
标签可以支配常量case
标签。例如,模式case
标签case Integer i
支配常量case
标签,当是类 type的成员时case 42
,模式case
标签case E e
支配常量case
标签。如果没有保护的相同模式标签支配常量标签,则受保护模式标签支配常量标签。换句话说,我们不检查守卫,因为这通常是不可判定的。例如,正如预期的那样,模式标签支配着常量标签;但霸占了标签。case A``A``enum``E``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
标签之前:
// As of Java 21
Integer i = ...
switch (i) {
case -1, 1 -> ... // Special cases
case Integer j when j > 0 -> ... // Positive integer cases
case Integer j -> ... // All the remaining integers
}
编译器检查所有case
标签。如果 switch 块中的标签被该 switch 块中任何前面的标签case
所支配,则这是一个编译时错误。case
此主导要求确保如果开关块仅包含类型模式case
标签,它们将 按子类型顺序出现。
(支配的概念类似于catch
语句子句的条件,如果捕获异常类的子句前面有一个可以捕获异常类的子句或( JLS §11.2.3 )的超类,try
则这是一个错误。逻辑上,前面的子句支配后面的子句。)catch``E``catch``E``E``catch``catch
switch
如果表达式或switch
语句的 switch 块具有多个全匹配 switch 标签,这也是一种编译时错误。全部匹配标签是default
模式case
标签,其中模式无条件匹配选择器表达式。例如,类型模式String s
无条件匹配类型的选择器表达式String
,并且类型模式Object o
无条件匹配任何引用类型的选择器表达式:
// As of Java 21
static void matchAll(String s) {
switch(s) {
case String t:
System.out.println(t);
break;
default:
System.out.println("Something else"); // Error - dominated!
}
}
static void matchAll2(String s) {
switch(s) {
case Object o:
System.out.println("An Object");
break;
default:
System.out.println("Something else"); // Error - dominated!
}
}
switch
表达式和陈述的详尽性
类型覆盖范围
表达式switch
要求选择器表达式的所有可能值都在 switch 块中处理;换句话说,它必须是_详尽无遗的_。这保持了switch
表达式的成功求值总是产生一个值的属性。
对于普通switch
表达式,此属性由 switch 块上的一组简单的额外条件强制执行。
对于模式表达式和语句,我们通过在 switch 块中定义 switch 标签的_类型覆盖_switch
概念来实现这一点。然后组合 switch 块中所有 switch 标签的类型覆盖率,以确定 switch 块是否耗尽了选择器表达式的所有可能性。
考虑这个(错误的)模式switch
表达式:
// As of Java 21
static int coverage(Object obj) {
return switch (obj) { // Error - not exhaustive
case String s -> s.length();
};
}
开关块只有一个开关标签case String s
。这匹配obj
其类型是 的子类型的任何值String
。因此,我们说这个开关标签的类型覆盖范围是 的每个子类型String
。该模式switch
表达式并不详尽,因为其 switch 块( 的所有子类型)的类型覆盖范围String
不包括选择器表达式 ( Object
) 的类型。
考虑这个(仍然是错误的)例子:
// As of Java 21
static int coverage(Object obj) {
return switch (obj) { // Error - still not exhaustive
case String s -> s.length();
case Integer i -> i;
};
}
该开关块的类型覆盖范围是其两个开关标签的覆盖范围的并集。换句话说,类型覆盖率是 的所有子类型的集合String
和 的所有子类型的集合Integer
。但是,类型覆盖仍然不包括选择器表达式的类型,因此该模式switch
表达式也不详尽,并会导致编译时错误。
标签的类型覆盖范围default
是每种类型,因此这个示例(终于!)合法:
// As of Java 21
static int coverage(Object obj) {
return switch (obj) {
case String s -> s.length();
case Integer i -> i;
default -> 0;
};
}
实践中的详尽性
类型覆盖的概念已经存在于非模式switch
表达式中。例如:
// As of Java 20
enum Color { RED, YELLOW, GREEN }
int numLetters = switch (color) { // Error - not exhaustive!
case RED -> 3;
case GREEN -> 5;
}
switch
枚举类的此表达式并不详尽,因为YELLOW
未涵盖预期的输入。正如预期的那样,添加一个case
标签来处理YELLOW
枚举常量足以使内容switch
详尽:
// As of Java 20
int numLetters = switch (color) { // Exhaustive!
case RED -> 3;
case GREEN -> 5;
case YELLOW -> 6;
}
以switch
这种方式编写的内容详尽无遗,有两个重要的好处。
首先,必须编写一个default
子句会很麻烦,它可能只会抛出异常,因为我们已经处理了所有情况:
int numLetters = switch (color) {
case RED -> 3;
case GREEN -> 5;
case YELLOW -> 6;
default -> throw new ArghThisIsIrritatingException(color.toString());
}
在这种情况下手动编写default
子句不仅令人恼火,而且实际上是有害的,因为编译器可以在没有子句的情况下更好地检查详尽性。 (对于任何其他全匹配子句,例如default
、case null, default
或无条件类型模式也是如此。)如果我们省略该default
子句,那么我们将在编译时发现是否忘记了标签case
,而不是在运行时发现 -也许即使到那时也不会。
更重要的是,如果有人后来向枚举添加另一个常量,会发生什么Color
?如果我们有一个显式的全部匹配子句,那么我们只会发现新的常量值(如果它在运行时出现)。但 是,如果我们对 进行编码switch
以覆盖编译时已知的所有常量,并省略 match-all 子句,那么下次重新编译包含switch
.匹配所有条款可能会导致彻底的详尽性错误。
结论:如果可能的话,switch
不带匹配所有子句的详尽的比带匹配所有子句的详尽的要好switch
。
看看运行时,如果Color
添加一个新常量,并且包含该常量的类switch
没有重新编译,会发生什么情况?新常数存在暴露给我们的风险switch
。因为这种风险始终存在于枚举中,所以如果详尽的枚举switch
没有匹配所有子句,那么编译器将合成一个default
引发异常的子句。这保证了switch
如果不选择其中一个子句就无法正常完成。
详尽性的概念旨在在覆盖所有合理情况与不强迫您编写许多罕见的极端情况之间取得平衡,这些情况会污染甚至主导您的代码而几乎没有实际价值。换句话说:穷举性是真正的运行时穷举性的编译时近似值。
详尽性和密封类
如果选择器表达式的类型是密封类(JEP 409),则类型覆盖检查可以考虑permits
密封类的子句来确定 switch 块是否是详尽的。这有时可以消除对条款的需要default
,如上所述,这是一个很好的做法。考虑以下接口示例,该sealed
接口S
具有三个允许的子类A
、B
和C
:
// As of Java 21
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
是一个密封接口,其允许的子类正是A
、B
和C
,因此该开关块是详尽的。因此,不需要default
标签。
当允许的直接子类仅实现(通用)超类的特定参数化时,需要额外小心sealed
。例如:
// As of Java 21
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
是A
和B
,但编译器可以检测到 switch 块只需要覆盖该类B
即可详尽,因为选择器表达式是 类型I<Integer>
并且没有参数化 是A
的子类型I<Integer>
。
再次强调,详尽性的概念是一个近似值。由于单独编译,接口的新颖实现有可能I
在运行时出现,因此编译器在这种情况下将插入一个default
抛出异常的合成子句。
由于记录模式可以嵌套,记录模式( JEP 440 )使详尽性的概念变得更加复杂。因此,详尽性的概念必须反映这种潜在的递归结构。
详尽性和兼容性
详尽性的要求适用于模式switch
表达式和模式switch
语句。为了确保向后兼容性,所有现有switch
语句将按原样进行编译。但是,如果switch
语句使用此 JEP 中描述的任何switch
增强功能,那么编译器将检查它是否详尽。 (Java 语言的未来编译器可能会针对switch
不详尽的遗留语句发出警告。)
switch
更准确地说,任何使用模式或null
标签或其选择器表达式不是旧类型(char
、byte
、short
、int
、Character
、Byte
、 、Short
、Integer
、String
或enum
类型)之一的语句都需要详尽。例如:
// As of Java 21
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;
};
}
模式变量声明的范围
模式变量(JEP 394)是由模式声明的局部变量。模式变量声明的不同寻常之处在于它们的作用域是_流敏感的_。回顾一下以下示例,其中类型模式String s
声明了模式变量s
:
// As of Java 21
static void testFlowScoping(Object obj) {
if ((obj instanceof String s) && s.length() > 3) {
System.out.println(s);
} else {
System.out.println("Not a string");
}
}
的声明位于模式变量将被初始化的s
代码部分的范围内。s
在此示例中,它位于表达式的右侧操作数&&
和“then”块中。但是,s
不在“else”块的范围内:为了将控制转移到“else”块,模式匹配必须失败,在这种情况下,模式变量将不会被初始化。
我们扩展了模式变量声明的这种流敏感的范围概念,以包含使用case
三个新规则的标签中出现的模式声明:
-
出现在受保护标签的模式中的模式变量声明的范围
case
包括保护,即when
表达式。 -
case
规则标签中出现的模式变量声明的范围包括出现在箭头右侧的switch
表达式、块或语句。throw
-
case
出现在带标签语句组的标签中的模式变量声明的范围switch
包括该语句组的块语句。case
禁止跌倒声明模式变量的标签。
此示例显示了第一条正在运行的规则:
// As of Java 21
static void testScope1(Object obj) {
switch (obj) {
case Character c
when c.charValue() == 7:
System.out.println("Ding!");
break;
default:
break;
}
}
模式变量声明的范围c
包括守卫,即表达式c.charValue() == 7
。
此变体显示了第二条规则的作用:
// As of Java 21
static void testScope2(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
第二个箭头右侧的语句。
第三条规则比较复杂。让我们首先考虑一个示例,其中带标签的语句组只有一个case
标签switch
:
// As of Java 21
static void testScope3(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
switch 标签并执行这些语句。
case
我们禁止掉入声明模式变量的标签的可能性。考虑这个错误的例子:
// As of Java 21
static void testScopeError(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;
}
}
如果允许这样做,并且 switch 块的 then 执行的值obj
可能Character
会落入第二个语句组 after case Integer i:
,其中模式变量i
将不会被初始化。因此,允许执行通过case
声明模式变量的标签是一个编译时错误。
case Character c: case Integer i: ...
这就是为什么不允许使用由多个模式标签组成的开关标签,例如。类似的推理也适用于禁止在单个case
标签内使用多种模式:既不case Character c, Integer i: ...
也case Character c, Integer i -> ...
不允许。如果case
允许这样的标签,则c
和i
都将在冒 号或箭头之后的范围内,但根据 的值是obj
aCharacter
还是 an ,只有其中一个会被初始化Integer
。
另一方面,穿过未声明模式变量的标签是安全的,如以下示例所示:
// As of Java 21
void testScope4(Object obj) {
switch (obj) {
case String s:
System.out.println("A string: " + s); // s in scope here!
default:
System.out.println("Done"); // s not in scope here
}
}
处理null
传统上,如果选择器表达式的计算结果为 ,则switch
抛出a 。这是众所周知的行为,我们不建议对任何现有代码进行更改。然而,对于模式匹配和值来说,存在合理且不会引发异常的语义,因此在模式切换块中,我们可以以更常规的方式进行处理,同时保持与现有语义的兼容。NullPointerException``null``switch``null``null``switch
首先,我们引入一个新的null
案例标签。然后,我们解除一揽子规则,如果选择器表达式的值为 ,则aswitch
立即抛出。相反,我们检查标签来确定 a 的行为:NullPointerException``null``case``switch
-
如果选择器表达式的计算结果为,
null
则任何null
case 标签都被认为是匹配的。如果没有与 switch 块关联的此类标签,则像以前一样switch
抛出。NullPointerException
-
如果选择器表达式的计算结果为非值,
null
那么我们case
照常选择一个匹配的标签。如果没有case
标签匹配,则任何default
标签都被视为匹配。
例如,给出下面的声明,评估nullMatch(null)
将打印null!
而不是抛出NullPointerException
:
// As of Java 21
static void nullMatch(Object obj) {
switch (obj) {
case null -> System.out.println("null!");
case String s -> System.out.println("String");
default -> System.out.println("Something else");
}
}
没有标签的 switch 块case null
被视为有一个case null
规则,其主体抛出NullPointerException
。换句话说,这段代码:
// As of Java 21
static void nullMatch2(Object obj) {
switch (obj) {
case String s -> System.out.println("String: " + s);
case Integer i -> System.out.println("Integer");
default -> System.out.println("default");
}
}
相当于:
// As of Java 21
static void nullMatch2(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");
}
}
在这两个示例中,评估nullMatch(null)
都会导致NullPointerException
抛出。
我们保留了现有结构的直觉switch
,即执行切换null
是一件特殊的事情。模式的区别switch
在于您可以直接在switch
.如果您null
在开关块中看到标签,则该标签将与某个null
值匹配。如果您在 switch 块中没有看到null
标签,那么切换null
值将像以前一样抛出NullPointerException
。null
因此,开关块中值的处理被规范化。
想要将null
case 与结合起来是有意义的,而且并不罕见default
。为此,我们允许null
案例标签有一个可选的default
;例如:
// As of Java 21
Object obj = ...
switch (obj) {
...
case null, default ->
System.out.println("The rest (including null)");
}
obj
如果 的值是空参考值,或者其他标签都不匹配,则该值与该标签匹配case
。
null
case
如果 switch 块同时具有带有 a 的标签default
和 adefault
标签,则会出现编译时错误。
错误
模式匹配可能会突然完成。例如,当将值与记录模式匹配时,记录的访问器方法可能会突然完成。在这种情况下,模式匹配被定义为通过抛出MatchException
.如果这样的模式作为 a 中的标签出现,switch
那么switch
也会通过抛出 a 来突然完成MatchException
。
如果一个case
模式有一个防护,并且评估防护突然完成,那么switch
出于同样的原因,评估也会突然完成。
如果模式中没有标签switch
与选择器表达式的值匹配,则switch
通过抛出 a 突然完成MatchException
,因为模式切换必须是详尽的。
例如:
// As of Java 21
record R(int i) {
public int i() { // bad (but legal) 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))
a 。 MatchException
(总是抛出异常的记录访问器方法是非常不规则的,而switch
抛出异常的详尽模式MatchException
是非常不寻常的。)
相比之下:
// As of Java 21
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))
an 。ArithmeticException
为了与模式switch
语义保持一致,类switch
的表达式enum
现在会抛出异常MatchException
,而不是IncompatibleClassChangeError
在运行时没有应用开关标签时抛出异常。这是对语言的一个小的不兼容的更改。 (仅当类在编译后发生更改时,switch
对枚举的穷举才无法匹配,这是非常不寻常的。)enum``switch
未来的工作
-
目前,pattern
switch
不支持原始类型boolean
、long
、float
和double
。允许这些基本类型还意味着允许它们出现在instanceof
表达式中,并将基本类型模式与引用类型模式对齐,这将需要大量的额外工作。这留给了未来可能的 JEP。 -
我们期望,将来,通用类将能够声明解构模式来指定它们如何匹配。这种解构模式可以与模式一起使用
switch
来生成非常简洁的代码。例如,如果我们有一个Expr
包含子类型 forIntExpr
(包含单个int
)、AddExpr
andMulExpr
(包含两个Expr
s)和NegExpr
(包含单个)的层次结构,我们可以一步Expr
匹配并作用于特定子类型:Expr
// Some future Java
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 e
、p if e
,甚至p &&& e
。 -
受保护模式标签的替代方案是直接支持_受保护模式_作为特殊模式形式,例如
p && e
。在早期的预览中对此进行了实验,由此产生的布尔表达式的歧义导致我们更喜欢受保护的case
标签而不是受保护的模式。
依赖关系
该 JEP基于 JDK 16 中提供的模式匹配instanceof
( JEP 394 ) 以及_Switch 表达式_( JEP 361 )提供的增强功能构建。它与_记录模式_(JEP 440 )共同演化。