Skip to content
微信扫码关注公众号

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 和强制转换的习惯用法,使其更加简洁且不易出错:

java
// 旧代码
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 测试链,比如:

java
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 标签带有模式而不仅仅是常量,那么我们可以更清晰和可靠地重写上面的代码:

java
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) 时间内执行分发。

switchnull

传统上,如果选择器表达式的值为 nullswitch 语句和表达式会抛出 NullPointerException,所以对 null 的测试必须在 switch 之外进行:

java
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 中会更好:

java
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 选择器。)

case 细化

switch 中对模式的试验表明,通常希望细化由模式标签体现的测试。例如,考虑下面这个对 Shape 值进行 switch 的代码:

java
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 的需要,这反过来意味着我们可以享受简洁的箭头风格(->)规则:

java
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 子句来做到这一点,一个带有守卫,一个没有:

java
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 比较):

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

主要的增强是引入一个新的 case 标签,case p,其中 p 是一个模式。switch 的本质没有改变:选择器表达式的值与 switch 标签进行比较,选择一个标签,然后执行或评估与该标签相关联的代码。现在的区别是,对于带有模式的 case 标签,选择的标签是由模式匹配的结果而不是由相等性测试决定的。例如,在下面的代码中,obj 的值匹配模式 Long l,并且与标签 case Long l 相关联的表达式被评估:

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

在成功的模式匹配之后,我们经常进一步测试匹配的结果。这可能导致繁琐的代码,比如:

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

不幸的是,所需的测试——obj 是长度为 1 的 String——被分割在模式 case 标签和后面的 if 语句之间。

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

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

第一个子句在 obj 既是 String 并且长度为 1 时匹配。第二个 caseobj 是任何长度的 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)、Stringenum 类型。我们扩展了这一点,并要求选择器表达式的类型是整数基本类型(不包括 long)或任何引用类型。

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

java
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 scase CharSequence cs 都将适用。

要解决的第一个问题是确切地确定在这种情况下应该应用哪个标签。我们不尝试复杂的最佳匹配方法,而是采用更简单的语义:选择在 switch 块中出现的第一个适用的值的 case 标签。

java
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 标签的顺序会发生什么呢?

java
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 类型,那么 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 标签仅当它的模式主导另一个模式 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 标签之前:

java
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 子句的条件,其中如果捕获异常类 Ecatch 子句前面有一个可以捕获 EE 的超类的 catch 子句,则是错误的(JLS §11.2.3)。从逻辑上讲,前面的 catch 子句主导后面的 catch 子句。)

对于 switch 表达式或 switch 语句的 switch 块有多个匹配所有的 switch 标签也是一个编译时错误。匹配所有的标签是 default 和模式 case 标签,其中模式无条件地匹配选择器表达式。例如,类型模式 String s 无条件地匹配类型为 String 的选择器表达式,类型模式 Object o 无条件地匹配任何引用类型的选择器表达式。

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

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

java
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 表达式:

java
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)。

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

java
static int coverage(Object obj) {
    return switch (obj) {         // 错误 - 仍然不详尽
        case String s  -> s.length();
        case Integer i -> i;
    };
}

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

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

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

如果选择器表达式的类型是一个密封类(JEP 409),那么类型覆盖检查可以考虑密封类的 permits 子句来确定一个 switch 块是否详尽。这有时可以消除对 default 子句的需要。考虑下面这个有三个允许的子类 ABC 的密封接口 S 的例子:

java
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 块的类型覆盖是类型 ABC。由于选择器表达式的类型 S 是一个密封接口,其允许的子类恰好是 ABC,所以这个 switch 块是详尽的。因此,不需要 default 标签。

当一个允许的直接子类仅实现一个(泛型的)密封超类的特定参数化时,需要一些额外的注意。例如:

java
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 的唯一允许的子类是 AB,但是编译器可以检测到,由于选择器表达式是类型 I<Integer>,所以 switch 块只需要覆盖类 B 就是详尽的。

这种详尽性的条件适用于模式 switch 表达式和模式 switch 语句。为了确保向后兼容性,所有现有的 switch 语句将无需更改地编译。但是如果一个 switch 语句使用了本 JEP 中描述的任何 switch 增强功能,那么编译器将检查它是否详尽。(未来的 Java 语言编译器可能会为不是详尽的传统 switch 语句发出警告。)

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

java
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 语句不是详尽的,并且是错误的:

java
Object obj =...
switch (obj) {    // 错误 - 不详尽!
    case String s:
        System.out.println(s);
        break;
    case Integer i:
        System.out.println("Integer");
        break;
}

它可以很容易地变得详尽:

java
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

java
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 标签向下贯穿。

这个例子展示了第一条规则的作用:

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

模式变量 c 的声明的作用域包括 switch 标签的 when 表达式。

这个变体展示了第二条规则的作用:

java
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 标签:

java
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 标签向下贯穿的可能性。考虑这个错误的例子:

java
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 标签是被允许的,那么 ci 在冒号或箭头后面都将在作用域内,但只有其中一个会被初始化,这取决于 obj 的值是一个 Character 还是一个 Integer

另一方面,从没有声明模式变量的标签向下贯穿是安全的,正如这个例子所示:

java
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 值有合理且不引发异常的语义,我们有机会使模式 switchnull 更友好,同时保持与现有的 switch 语义兼容。

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

  • 如果选择器表达式的值为 null,那么任何 null case 标签都被认为是匹配的。如果 switch 块中没有这样的标签,那么 switch 会像以前一样抛出 NullPointerException
  • 如果选择器表达式的值为非 null 值,那么我们像往常一样选择一个匹配的 case 标签。如果没有 case 标签匹配,那么任何 default 标签都被认为是匹配的。

例如,给定下面的声明,评估 test(null) 将打印 null! 而不是抛出 NullPointerException

java
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 的新行为就好像编译器自动用一个主体会抛出 NullPointerExceptioncase null 来丰富 switch 块。换句话说,这段代码:

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

等同于:

java
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;例如:

java
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 必须是详尽的。

例如:

java
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

相比之下:

java
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 不支持基本类型 booleanlongfloatdouble。它们的实用性似乎很小,但可以添加对这些类型的支持。

  • 我们期望在未来,普通类将能够声明解构模式来指定它们如何被匹配。这样的解构模式可以与模式 switch 一起使用,以产生非常简洁的代码。例如,如果我们有一个 Expr 的层次结构,其中有 IntExpr(包含一个 int)、AddExprMulExpr(包含两个 Expr)以及 NegExpr(包含一个 Expr)的子类型,我们可以在一步中对 Expr 进行匹配并对特定子类型采取行动:

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

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

  • 添加“与”和“或”模式也可能是有用的,以允许带有模式的 case 标签有更多的表现力。

替代方案

  • 与其支持模式 switch,我们可以定义一个仅支持对选择器表达式的类型进行 switch 的“类型 switch”。这个特性更容易指定和实现,但表现力要弱得多。
  • 对于有守卫的模式标签,有许多其他语法选项,例如 p where ep if e,甚至 p &&& e
  • 有守卫的模式标签的替代方案是直接支持“有守卫的模式”作为一种特殊的模式形式,例如 p && e。在以前的预览中对这个进行了试验,结果与布尔表达式的歧义导致我们在模式 switch 中更喜欢 when 子句。

依赖关系

这个 JEP 建立在 instanceof 的模式匹配(JEP 394)以及 switch 表达式提供的增强功能(JEP 361)之上。当记录模式预览特性(JEP 432)最终确定时,最终的实现可能会使用动态常量(JEP 309)。