JEP 433: Pattern Matching for switch (Fourth Preview) | switch 语句中的模式匹配(第四次预览)
摘要
通过为 switch
表达式和语句添加模式匹配来增强 Java 编程语言。将模式匹配扩展到 switch
允许对一个表达式针对多个模式进行测试,每个模式都有特定的动作,从而可以简洁且安全地表达复杂的数据导向查询。这是一个 预览语言特性。
历史
switch
的模式匹配由 JEP 406 作为预览特性提出,并在 JDK 17 中交付,由 JEP 420 提议进行第二次预览并在 JDK 18 中交付,由 JEP 427 提议进行第三次预览并在 JDK 19 中交付。这个 JEP 提议进行第四次预览,以使其与记录模式预览特性(JEP 432)继续共同演进,并允许基于持续的经验和反馈进行其他改进。
自第三次预览以来的主要变化有:
对一个枚举类进行详尽的
switch
(即switch
表达式或模式switch
语句)现在如果在运行时没有switch
标签适用,则抛出MatchException
而不是IncompatibleClassChangeError
。switch
标签的语法更简单。在
switch
表达式和语句中,以及支持模式的其他结构中,现在支持对通用记录模式的类型参数进行推断。
目标
通过允许在
case
标签中除了常量外还包含模式和null
来扩展switch
表达式和语句的表现力和适用性。在需要时放松
switch
传统上对null
的不友好性。通过要求模式
switch
语句涵盖所有可能的输入值来增加switch
语句的安全性。确保所有现有的
switch
表达式和语句在无需更改的情况下继续编译,并以相同的语义执行。
动机
在 Java 16 中,JEP 394 扩展了 instanceof
运算符以接受一个“类型模式”并执行“模式匹配”。这个适度的扩展允许简化熟悉的 instanceof
和强制转换的习惯用法,使其更加简洁且不易出错:
// 旧代码
if (obj instanceof String) {
String s = (String)obj;
... 使用 s...
}
// 新代码
if (obj instanceof String s) {
... 使用 s...
}
我们经常想要针对多个替代方案来比较一个变量,比如 obj
。Java 支持使用 switch
语句进行多路比较,并且自 Java 14 以来,支持使用 switch
表达式(JEP 361),但不幸的是 switch
非常有限。你只能对少数几种类型进行 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
赋值,我们就有一个错误。(将 formatted
声明为一个空白局部变量至少可以让编译器的确定赋值分析参与进来,但开发人员并不总是编写这样的声明。)此外,上面的代码不是可优化的;如果没有编译器的特殊处理,它将具有 O(n) 的时间复杂度,尽管底层问题通常是 O(1)。
但是 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
的语义很清楚:带有模式的 case
标签在选择器表达式 obj
的值与模式匹配时适用。(为了简洁我们展示了一个 switch
表达式,但也可以展示一个 switch
语句;switch
块,包括 case
标签,将保持不变。)
这段代码的意图更清晰,因为我们使用了正确的控制结构:我们在说,“参数 obj
最多匹配以下条件中的一个,找出它并评估相应的分支。”作为一个额外的好处,它更易于优化;在这种情况下,我们更有可能能够在 O(1) 时间内执行分发。
switch
与 null
传统上,如果选择器表达式的值为 null
,switch
语句和表达式会抛出 NullPointerException
,所以对 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
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 null
,switch
将执行与该标签相关联的代码;如果没有 case null
,switch
将像以前一样抛出 NullPointerException
。(为了与 switch
的当前语义保持向后兼容,default
标签不匹配一个 null
选择器。)
case
细化
在 switch
中对模式的试验表明,通常希望细化由模式标签体现的测试。例如,考虑下面这个对 Shape
值进行 switch
的代码:
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
来获得正确的行为。(注意在 if
块内部小心地放置 break
语句。)
这里的问题是,使用单一模式来区分情况在单个条件之外不能很好地扩展——我们需要一些方法来表达对模式的细化。因此,我们允许在 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
子句来做到这一点,一个带有守卫,一个没有:
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
语句之间。
为了解决这个问题,我们通过在模式标签后面支持一个可选的守卫来引入“受保护的模式 case
标签”。这允许上面的代码被重写,以便所有的条件逻辑都被提升到 switch
标签中:
static void test(Object obj) {
switch (obj) {
case String s when s.length() == 1 ->...
case String s ->...
...
}
}
第一个子句在 obj
既是 String
并且长度为 1 时匹配。第二个 case
在 obj
是任何长度的 String
时匹配。
只有模式标签可以有守卫。例如,写一个带有 case
常量和守卫的标签是无效的;例如,case "Hello" when RandomBooleanExpression()
。
有时我们需要用括号括起模式以提高可读性。因此,我们扩展模式的语言以支持用 (p)
表示的“括号模式”,其中 p
是一个模式。一个括号模式 (p)
引入了由子模式 p
引入的模式变量。一个值匹配一个括号模式 (p)
如果它匹配模式 p
。
在支持 switch
中的模式时,有五个主要的语言设计方面需要考虑:
- 增强的类型检查
switch
表达式和语句的详尽性- 模式变量声明的作用域
- 处理
null
- 错误
1. 增强的类型检查
1a. 选择器表达式的类型
在 switch
中支持模式意味着我们可以放松当前对选择器表达式类型的限制。目前,普通 switch
的选择器表达式的类型必须是整数基本类型(不包括 long
)、相应的装箱形式(即 Character
、Byte
、Short
或 Integer
)、String
或 enum
类型。我们扩展了这一点,并要求选择器表达式的类型是整数基本类型(不包括 long
)或任何引用类型。
例如,在下面的模式 switch
中,选择器表达式 obj
与涉及类类型、enum
类型、记录类型和数组类型的类型模式进行匹配,同时还有一个 null
case
标签和一个 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
标签 case String s
和 case CharSequence cs
都将适用。
要解决的第一个问题是确切地确定在这种情况下应该应用哪个标签。我们不尝试复杂的最佳匹配方法,而是采用更简单的语义:选择在 switch
块中出现的第一个适用的值的 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 -> // 错误 - 模式被前面的模式主导
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
。
一个有守卫的模式 case
标签仅当它的模式主导另一个模式 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 ->... // 特殊情况
case Integer i when i > 0 ->... // 正整数情况
case Integer i ->... // 所有剩余的整数
}
编译器检查所有的 case
标签。在 switch
块中的一个 case
标签被该 switch
块中的前面的 case
标签主导是一个编译时错误。这个主导要求确保如果一个 switch
块仅包含类型模式 case
标签,它们将以子类型顺序出现。
(主导的概念类似于 try
语句的 catch
子句的条件,其中如果捕获异常类 E
的 catch
子句前面有一个可以捕获 E
或 E
的超类的 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) ->
... // 推断的记录模式 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) { // 错误 - 不详尽
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) { // 错误 - 仍然不详尽
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
子句的需要。考虑下面这个有三个允许的子类 A
、B
和 C
的密封接口 S
的例子:
sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {} // 隐式最终类
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
,所以这个 switch
块是详尽的。因此,不需要 default
标签。
当一个允许的直接子类仅实现一个(泛型的)密封超类的特定参数化时,需要一些额外的注意。例如:
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) {
// 详尽的,因为不可能有 A 的情况!
case B<Integer> bi -> 42;
}
}
I
的唯一允许的子类是 A
和 B
,但是编译器可以检测到,由于选择器表达式是类型 I<Integer>
,所以 switch
块只需要覆盖类 B
就是详尽的。
这种详尽性的条件适用于模式 switch
表达式和模式 switch
语句。为了确保向后兼容性,所有现有的 switch
语句将无需更改地编译。但是如果一个 switch
语句使用了本 JEP 中描述的任何 switch
增强功能,那么编译器将检查它是否详尽。(未来的 Java 语言编译器可能会为不是详尽的传统 switch
语句发出警告。)
更准确地说,任何使用模式或 null
标签或其选择器表达式不是传统类型(char
、byte
、short
、int
、Character
、Byte
、Short
、Integer
、String
或枚举类型)的 switch
语句都需要是详尽的。例如:
sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {} // 隐式最终类
static void switchStatementExhaustive(S s) {
switch (s) { // 错误 - 不详尽;
// 缺少允许的类 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) { // 错误 - 不详尽!
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: // 现在详尽了!
break;
}
由于记录模式(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
标签中出现的模式声明,有三条新规则:
- 出现在
switch
标签中的模式变量声明的作用域包括该标签的任何when
子句。 - 在
switch
规则的case
标签中出现的模式变量声明的作用域包括出现在箭头右侧的表达式、块或throw
语句。 - 在
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
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");
case Integer i: // 编译时错误
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
标签是被允许的,那么 c
和 i
在冒号或箭头后面都将在作用域内,但只有其中一个会被初始化,这取决于 obj
的值是一个 Character
还是一个 Integer
。
另一方面,从没有声明模式变量的标签向下贯穿是安全的,正如这个例子所示:
void test(Object obj) {
switch (obj) {
case String s:
System.out.println("A string");
default:
System.out.println("Done");
}
}
4. 处理 null
传统上,如果选择器表达式的值为 null
,switch
会抛出 NullPointerException
。这是一种被很好理解的行为,我们不建议对任何现有的 switch
代码改变这种行为。
然而,鉴于对于模式匹配和 null
值有合理且不引发异常的语义,我们有机会使模式 switch
对 null
更友好,同时保持与现有的 switch
语义兼容。
首先,我们引入一个新的 null
case
标签。然后,我们取消了如果选择器表达式的值为 null
,switch
立即抛出 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
的新行为就好像编译器自动用一个主体会抛出 NullPointerException
的 case null
来丰富 switch
块。换句话说,这段代码:
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
case
标签有一个可选的 default
;例如:
Object obj =...
switch (obj) {
...
case null, default ->
System.out.println("The rest (including null)");
}
如果 obj
的值是 null
引用值,或者其他 case
标签都不匹配,那么 obj
的值就匹配这个标签。
如果一个 switch
块既有一个带有 default
的 null
case
标签,又有一个单独的 default
标签,那是一个编译时错误。
5. 错误
模式匹配可能会突然结束。例如,当将一个值与记录模式进行匹配时,记录的访问器方法可能会突然结束。在这种情况下,模式匹配被定义为通过抛出 MatchException
来突然结束。如果这样的模式作为 switch
中的标签出现,那么 switch
也将通过抛出 MatchException
而突然结束。
如果一个模式用 when
表达式进行守卫,并且对 when
表达式的求值突然结束,那么 switch
也会因为同样的原因突然结束。
如果在模式 switch
中没有标签与选择器表达式的值匹配,那么 switch
将通过抛出 MatchException
而突然结束,因为模式 switch
必须是详尽的。
例如:
record R(int i){
public int i(){ // 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
标签适用时,对枚举类的 switch
表达式现在抛出 MatchException
而不是 IncompatibleClassChangeError
。这是对语言的一个小的不兼容更改。
未来工作
目前,模式
switch
不支持基本类型boolean
、long
、float
和double
。它们的实用性似乎很小,但可以添加对这些类型的支持。我们期望在未来,普通类将能够声明解构模式来指定它们如何被匹配。这样的解构模式可以与模式
switch
一起使用,以产生非常简洁的代码。例如,如果我们有一个Expr
的层次结构,其中有IntExpr
(包含一个int
)、AddExpr
和MulExpr
(包含两个Expr
)以及NegExpr
(包含一个Expr
)的子类型,我们可以在一步中对Expr
进行匹配并对特定子类型采取行动:javaint 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(); }; }
如果没有这样的模式匹配,像这样表达临时的多态计算需要使用繁琐的 访问者模式。模式匹配通常更加透明和直接。
添加“与”和“或”模式也可能是有用的,以允许带有模式的
case
标签有更多的表现力。
替代方案
- 与其支持模式
switch
,我们可以定义一个仅支持对选择器表达式的类型进行switch
的“类型switch
”。这个特性更容易指定和实现,但表现力要弱得多。 - 对于有守卫的模式标签,有许多其他语法选项,例如
p where e
、p if e
,甚至p &&& e
。 - 有守卫的模式标签的替代方案是直接支持“有守卫的模式”作为一种特殊的模式形式,例如
p && e
。在以前的预览中对这个进行了试验,结果与布尔表达式的歧义导致我们在模式switch
中更喜欢when
子句。
依赖关系
这个 JEP 建立在 instanceof
的模式匹配(JEP 394)以及 switch
表达式提供的增强功能(JEP 361)之上。当记录模式预览特性(JEP 432)最终确定时,最终的实现可能会使用动态常量(JEP 309)。