Skip to content

JEP 427: Pattern Matching for switch (Third Preview) | switch 的模式匹配(第三个预览)

摘要

通过在 switch 表达式和语句中引入模式匹配来增强 Java 编程语言。将模式匹配扩展到 switch 允许一个表达式针对多个模式进行测试,每个模式都有特定的动作,这样就可以简洁且安全地表达复杂的数据导向查询。这是一个预览语言特性

历史

模式匹配在 switch 中作为预览特性由 JEP 406 提出,并在 JDK 17 中实现;之后由 JEP 420 提议进行第二次预览,并在 JDK 18 中实现。本 JEP 提议进行第三次预览,基于持续的经验和反馈进一步完善此特性。

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

  • switch 块中用 when 子句替换了受保护的模式。

  • switch 选择器表达式的值为 null 时,其运行时语义更接近于传统 switch 的行为。

目标

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

  • 允许按需放松 switchnull 的不友好性。

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

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

动机

在 Java 16 中,JEP 394 扩展了 instanceof 运算符以接受 类型模式 并执行 模式匹配。这一小幅度的扩展使得熟悉的 instanceof 和类型转换的惯用法得以简化:

java
// 旧代码
if (o instanceof String) {
    String s = (String)o;
    ... 使用 s ...
}

// 新代码
if (o instanceof String s) {
    ... 使用 s ...
}

我们经常需要将一个变量(如 o)与多个选项进行比较。Java 支持使用 switch 语句和 switch 表达式(自从 Java 14 开始,参见 JEP 361)进行多路比较,但遗憾的是 switch 非常有限。你只能对几种类型的值进行切换——整数基本类型(不包括 long)、它们对应的装箱形式、枚举类型以及 String——并且只能测试这些值是否等于常量。我们可能希望使用模式来测试同一个变量与多种可能性,并对每种可能性采取特定的动作,但由于现有的 switch 不支持这一点,最终我们不得不使用一系列的 if...else 测试,例如:

java
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 表达式中受益,但它远非完美。首先也是最重要的一点是,这种方法可能会让编码错误隐藏起来,因为我们使用了一个过于通用的控制结构。我们的意图是在 if...else 链的每一部分都给 formatted 赋值,但是没有任何机制能让编译器识别并验证这个不变性。如果某个块——可能是很少被执行的那个——没有给 formatted 赋值,那么就会出现一个 bug。(将 formatted 声明为空局部变量至少可以让编译器的确定赋值分析发挥作用,但这并不是开发人员总是会做的。)此外,上述代码并不易于优化;即使编译器做出特殊努力,它的时间复杂度也将是 O(n),即便实际上底层问题往往是 O(1)

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

java
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 的语义很清楚:当 switch 选择器表达式的值 o 匹配 case 标签中的模式时,该 case 标签适用。(为了简短起见,这里展示的是一个 switch 表达式,但也可以展示一个 switch 语句;switch 块,包括 case 标签,将保持不变。)

这段代码的意图更加明确,因为我们使用了正确的控制结构:我们说的是,“参数 o 最多符合以下条件之一,找出符合条件的那个并执行相应的分支。”另外,这种写法还具有可优化性;在这种情况下,我们更有可能在 O(1) 时间内完成调度。

模式 switchnull

传统上,如果 switch 语句或表达式的选择器表达式求值为 null,则 switch 会抛出 NullPointerException,因此必须在 switch 之外进行 null 测试:

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 测试集成到 switch 中,允许使用一个新的 null case 标签:

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

我们可能希望将 null 与其他 case 标签组合在一起。例如,在下面的代码中,case null, String s 标签既匹配 null 值也匹配所有 String 值:

java
static void testStringOrNull(Object o) {
    switch (o) {
        case null, String s -> System.out.println("String: " + s);
        default -> System.out.println("Something else");
    }
}

case 标签细化

switch 中使用模式的实验表明,通常需要细化由模式标签所体现的测试。例如,考虑下面的代码,它根据 Shape 类型的值进行切换:

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 标签,然后不太舒服地将三角形面积的测试放在相应的语句组中。接着我们需要使用穿透(fall-through)来确保当三角形面积小于 100 时得到正确的行为。(注意 break 语句在 if 块内的精心放置。)

这里的问题在于,仅使用单一模式来区分不同的情况无法满足多个条件的要求——我们需要一种方式来表达对模式的细化。一种方法是引入 受保护的模式,表示为 p && b,其中允许模式 p 通过任意布尔表达式 b 来细化。

我们在本 JEP 的前版本中实现了受保护的模式。基于经验和反馈,我们建议改用 switch 块中的 when 子句来为模式标签指定保护条件,例如 case Triangle t when t.calculateArea() > 100。我们将这样的模式标签称为 受保护的 模式标签,而布尔表达式称为 保护条件

采用这种方式,我们可以重新审视 testTriangle 代码以直接表达大型三角形的特殊情况。这消除了 switch 语句中对穿透的使用,进而意味着我们可以享受简洁的箭头风格(->)规则:

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 标签中的模式

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

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

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

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

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

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

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

第一条子句匹配当 o 既是 String 且长度为 1 的情况。第二条子句匹配 o 是任何长度的 String 的情况。

只有模式标签可以有保护条件。例如,不允许在常量 case 标签后面加上保护条件,例如 case "Hello" when RandomBooleanExpression()

有时我们需要使用括号来提高模式的可读性。因此,我们扩展了模式的语言来支持 括号内的模式,写作 (p),其中 p 是一个模式。括号内的模式 (p) 引入了子模式 p 所引入的所有模式变量。一个值如果匹配模式 p 则匹配括号内的模式 (p)

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

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

1. 增强的类型检查

1a. 选择器表达式的类型

支持 switch 中的模式意味着我们可以放宽对选择器表达式类型的现有限制。目前,常规 switch 的选择器表达式的类型必须是整数基本类型(不包括 long)、对应的装箱形式(即 CharacterByteShortInteger)、String 或者 enum 类型。我们扩展了这一点,并要求选择器表达式的类型要么是整数基本类型(不包括 long),要么是任何引用类型。

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

java
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 块中首先出现的适用于某个值的模式标签将被选中。

java
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 类型,则第二个模式标签将适用。

但如果我们将这两个模式标签的顺序颠倒会发生什么?

java
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 的值是 String 类型,那么 CharSequence 模式标签将适用,因为它首先出现在 switch 块中。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

受保护的模式标签仅在它的模式主导另一个模式标签的模式 并且 它的保护条件是一个值为 true 的常量表达式的情况下主导另一个模式标签(无论是受保护还是未受保护)。例如,受保护的模式标签 case String s when true 主导模式标签 case String s。我们不对保护条件的表达式进行更深入的分析来更精确地确定哪些值匹配模式标签(这个问题在一般情况下是不可判定的)。

模式标签可以主导常量标签。例如,模式标签 case Integer i 主导常量标签 case 42,而模式标签 case E e 主导常量标签 case AA 是枚举类型 E 的枚举常量。如果去掉受保护模式标签的 when 子句后,该模式标签仍然可以主导常量标签。换句话说,我们不会检查保护条件,因为一般来说这是不可判定的。例如,模式标签 case String s when s.length() > 1 如预期那样主导常量标签 case "hello";但 case Integer i when i != 0 主导标签 case 0

所有这些提示了一种简单、可预测且易于阅读的 case 标签排序方式:常量标签应出现在受保护的模式标签之前,而受保护的模式标签应出现在未受保护的模式标签之前:

java
Integer i = ...
switch (i) {
    case -1, 1 -> ...                 // 特殊情况
    case Integer i when i > 0 -> ...  // 正整数情况
    case Integer i -> ...             // 所有剩余的整数
}

编译器会检查所有标签。如果 switch 块中的一个标签被该块中的先前标签主导,则这是一种编译时错误。这一主导性要求确保了如果 switch 块只包含类型模式 case 标签,它们将按照子类型顺序出现。

(主导性的概念类似于 try 语句中 catch 子句的条件,在这种情况下,如果一个捕获异常类 Ecatch 子句被一个可以捕获 E 或其超类的 catch 子句所前置,则视为错误 (JLS §11.2.3)。逻辑上,前置的 catch 子句主导后续的 catch 子句。)

对于 switch 表达式或 switch 语句的 switch 块来说,如果它包含多个全匹配 switch 标签,这也是一种编译时错误。全匹配标签是 default 和那些模式无条件匹配选择器表达式的模式标签。例如,类型模式 String s 无条件匹配类型为 String 的选择器表达式,而类型模式 Object o 无条件匹配任何引用类型的选择器表达式。

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

switch 表达式要求在 switch 块中处理选择表达式的所有可能值;换句话说,它必须是“完备”的。这样可以保持一个属性,即成功评估 switch 表达式总会产生一个值。对于普通的 switch 表达式,这是通过一组对 switch 块的额外条件来强制实现的。

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

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

java
static int coverage(Object o) {
    return switch (o) {         // 错误 - 不是完备的
        case String s -> s.length();
    };
}

switch 块只有一个 switch 标签 case String s。这个标签匹配任何类型为 String 或其子类型的值。因此,我们说这个 switch 标签的类型覆盖是 String 及其所有子类型。这个模式匹配 switch 表达式不是完备的,因为 switch 块的类型覆盖(所有 String 的子类型)并没有包含选择表达式的类型(Object)。

再考虑下面这个(仍然错误的)示例:

java
static int coverage(Object o) {
    return switch (o) {         // 错误 - 依然不是完备的
        case String s  -> s.length();
        case Integer i -> i;
    };
}

这个 switch 块的类型覆盖是其两个 switch 标签覆盖的并集。换句话说,类型覆盖是所有 String 子类型和所有 Integer 子类型的集合。但是,类型覆盖仍然没有包括选择表达式的类型,所以这个模式匹配 switch 表达式同样不是完备的,并且会导致编译时错误。

default 标签的类型覆盖是所有类型,因此下面的例子(终于!)是合法的:

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

在这个例子中,switch 块包含了针对 StringInteger 类型的特定情况处理以及一个 default 分支,后者覆盖了所有其他类型。因此,这个 switch 表达式是完备的,可以正确地处理 Object 类型的任何实例。

如果选择表达式的类型是一个密封类(JEP 409),那么类型覆盖检查可以考虑该密封类的 permits 子句来确定 switch 块是否完备。这有时可以省去 default 子句的需求。考虑以下示例,其中有一个密封接口 S,它允许三个子类 ABC

java
sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {}  // 隐式地为 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 超类时,需要特别注意。例如:

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,但编译器可以检测到 switch 块只需要覆盖 B 类即可达到完备性,因为选择表达式是类型 I<Integer>

为了防止不兼容的独立编译,在密封类上的 switch 中,如果 switch 块是完备的并且没有匹配所有情况的分支,则编译器会自动添加一个 default 标签,其代码会抛出 IncompatibleClassChangeError 异常。这个标签仅在密封接口被更改而 switch 代码未重新编译的情况下才会被触发。实际上,编译器为你加强了代码。

这种完备性的条件适用于模式匹配 switch 表达式和模式匹配 switch 语句。为了确保向后兼容性,所有现有的 switch 语句将不变地编译。但是如果 switch 语句使用了本 JEP 中详述的任何 switch 增强功能,则编译器会检查它是否完备。(未来的 Java 语言编译器可能会为不完备的遗留 switch 语句发出警告。)

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

java
sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {}  // 隐式地为 final

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 o = ...
switch (o) {    // 错误 - 不是完备的!
    case String s:
        System.out.println(s);
        break;
    case Integer i:
        System.out.println("Integer");
        break;
}

它可以非常简单地变为完备的:

java
Object o = ...
switch (o) {
    case String s:
        System.out.println(s);
        break;
    case Integer i:
        System.out.println("Integer");
        break;
    default:    // 现在是完备的!
        break;
}

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

3. 模式变量声明的作用域

模式变量JEP 394)是由模式声明的局部变量。模式变量声明的一个不同寻常之处在于它们的作用域是流敏感的。作为回顾,考虑以下示例,其中类型模式 String s 声明了模式变量 s

java
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. 发生在 switch 规则的 case 标签中的模式变量声明的作用域包括出现在箭头右侧的表达式、块或 throw 语句。

  3. 发生在 switch 标记语句组的 case 标签中的模式变量声明的作用域包括语句组的块语句。从声明了模式变量的 case 标签直接跳转到下一个 case 标签是不允许的。

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

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

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

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

java
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 的声明作用域是第一个箭头右侧的块。模式变量 i 的声明作用域是第二个箭头右侧的 throw 语句。

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

java
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 语句组的语句,即使第一个语句组的执行可以跳过 default switch 标签并执行这些语句。

我们禁止从声明了模式变量的 case 标签直接跳转。考虑这个错误的例子:

java
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:                 // 编译时错误
            System.out.println("An integer " + i);
        default:
            break;
    }
}

如果这种情况被允许,并且选择表达式 o 的值是 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 都将处于有效作用域内,但根据 o 的值是 Character 还是 Integer,只会有一个被初始化。

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

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

4. 处理 null

4a. 匹配 null

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

然而,鉴于存在一种合理且不会抛出异常的方式来处理模式匹配和 null 值,我们有机会让模式 switch 更加友好地处理 null,同时保持与现有 switch 语义的兼容性。

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

  • 如果选择表达式计算结果为 null,则任何 null 标签被认为匹配。如果没有与 switch 块相关联的此类标签,则 switch 仍会抛出 NullPointerException,如同以往。

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

例如,给定以下声明,计算 test(null) 将打印 null! 而不是抛出 NullPointerException

java
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。换句话说,这段代码:

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

等价于:

java
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 中的不同之处在于,你有了一个机制可以直接在 switch 内部处理这种情况,而不是在外部处理。如果你在 switch 块中看到一个 null 标签,那么这个标签将匹配 null 值。如果你在 switch 块中没有看到 null 标签,则对 null 值执行 switch 会抛出 NullPointerException,如同以往一样。

4b. 由 null 标签产生的新标签形式

自 Java 16 起,switch 块支持两种风格:一种基于标记语句组(: 形式),其中允许直接跳转;另一种基于单个结果形式(-> 形式),其中不允许直接跳转。在前一种风格中,多个标签通常写为 case l1: case l2:,而在后一种风格中,多个标签写为 case l1, l2 ->

支持 null 标签意味着可以在 : 形式中表达一些特殊情况。例如:

java
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 -> 标签,如下所示:

java
Object o = ...
switch (o) {
    case null, String s -> System.out.println("String, including null");
    ...
}

o 的值要么是 null 引用,要么是 String 时,该值与这个标签匹配。在这两种情况下,模式变量 s 都会被 o 的值初始化。(相反的形式 case String s, null 也是允许的,并且行为相同。)

null 标签与 default 标签组合也是有意义且常见的,即:

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

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

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

o 的值与这个标签匹配,如果它是 null 引用值,或者没有其他标签匹配。

未来工作

  • 当前,模式 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();
         };
    }

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

  • 可能还有必要增加 AND 和 OR 模式,以增强 case 标签中模式的表达能力。

替代方案

  • 我们可以选择定义一个 类型 switch,仅支持根据选择表达式的类型进行切换。这个特性更容易规范和实现,但表达力显著较低。

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

  • 除了带条件的模式标签之外,还可以直接支持 带条件的模式 作为一种特殊的模式形式,例如 p && e。在之前的预览版本中尝试过这种方法,但由此导致的与布尔表达式的歧义使我们倾向于在模式 switch 中使用 when 子句。

依赖项

此 JEP 建立在 instanceof 的模式匹配 (JEP 394) 之上,并利用 switch 表达式提供的增强功能 (JEP 361)。当 JEP 405(记录模式)出现时,最终实现很可能利用动态常量 (JEP 309)。