Skip to content

JEP 441: Pattern Matching for switch | 用于 switch 的模式匹配

摘要

通过为 switch 表达式和语句添加模式匹配功能来增强 Java 编程语言。将模式匹配扩展到 switch 允许表达式与多个模式进行匹配,每个模式都有特定的操作,从而可以简洁且安全地表达复杂的数据导向查询。

历史

此功能最初由 JEP 406(JDK 17)提出,随后在 JEPs 420(JDK 18)、427(JDK 19)和 433(JDK 20)中进行了完善。它与 记录模式(Record Patterns)特性(JEP 440)共同演进,两者之间存在大量交互。本 JEP 提议在继续获得经验和反馈的基础上,通过进一步的小幅改进来最终确定该功能。

除了各种编辑性更改外,与之前的 JEP 相比,主要更改包括:

  • 移除带括号的模式,因为它们没有足够的价值,

  • 允许在 switch 表达式和语句的 case 常量中使用限定枚举常量。

目标

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

  • 在需要时,放宽 switch 对空值的敌意。

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

  • 确保所有现有的 switch 表达式和语句在无需更改的情况下继续编译,并以相同的语义执行。

动机

在 Java 16 中,JEP 394 扩展了 instanceof 操作符,使其能够接受一个 类型模式 并执行 模式匹配。这一小小的扩展允许将熟悉的 instanceof-and-cast 惯用法简化,使其更加简洁且不易出错:

java
// 在 Java 16 之前
if (obj instanceof String) {
    String s = (String)obj;
    ... 使用s ...
}

// 从 Java 16 开始
if (obj instanceof String s) {
    ... 使用s ...
}

在新代码中,如果运行时 obj 的值是 String 的实例,则 obj 与类型模式 String s 匹配。如果模式匹配,则 instanceof 表达式的值为 true,并且模式变量 s 被初始化为 obj 转换为 String 类型的值,然后可以在包含的代码块中使用它。

我们经常需要将一个变量(如 obj)与多个备选值进行比较。Java 通过 switch 语句支持多路比较,并且自 Java 14 起,还引入了 switch 表达式(JEP 361),但不幸的是,switch 的用途非常有限。我们只能对少数类型的值进行切换——整型基本类型(不包括 long)、它们对应的装箱形式、enum 类型和 String——并且我们只能对常量进行精确相等性测试。我们可能希望使用模式来测试同一变量是否符合多个可能性,并对每个可能性执行特定操作,但由于现有的 switch 不支持这一功能,我们最终会得到一个 if...else 测试链,如下所示:

java
// 在 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 表达式,但它远非完美。首先,这种方法允许编码错误隐藏,因为我们使用了过于通用的控制结构。目的是在每个 if...else 链的分支中为 formatted 分配值,但没有任何东西能够使编译器识别并强制执行这一不变性。如果某个 “then” 块(可能是执行频率较低的块)没有为 formatted 赋值,我们就会遇到错误。(将 formatted 声明为空白局部变量至少可以让编译器在确定赋值分析中参与这项工作,但开发人员并不总是编写这样的声明。)此外,上述代码不可优化;即使底层问题通常是 O(1) 的,但在没有编译器优化的情况下,它仍将具有 O(n) 的时间复杂度。

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

java
// 从 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 的语义很清晰:如果选择器表达式 obj 的值与模式匹配,则应用带有该模式的 case 标签。(为了简洁起见,我们展示了一个 switch 表达式,但也可以展示一个 switch 语句;switch 块,包括 case 标签,将保持不变。)

这段代码的意图更清晰,因为我们使用了正确的控制结构:我们说,“参数 obj 最多匹配以下条件之一,请找出并评估对应的分支。” 此外,它也更易于优化;在这种情况下,我们更有可能以 O(1) 的时间进行分派。

Switch 语句和 null

传统上,如果选择器表达式计算结果为 null,则 switch 语句和表达式会抛出 NullPointerException,因此必须在 switch 外部测试 null

java
// 在 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 案例标签,将 null 测试集成到 switch 中:

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

当选择器表达式的值为 null 时,switch 的行为始终由其 case 标签决定。有了 case nullswitch 会执行与该标签关联的代码;如果没有 case null,则 switch 会抛出 NullPointerException,就像以前一样。(为了保持与当前 switch 语义的向后兼容性,default 标签不会与 null 选择器匹配。)

案例细化

与带有常量的 case 标签相比,模式 case 标签可以应用于多个值。这通常会导致在 switch 规则的右侧出现条件代码。例如,考虑以下代码:

java
// 从 Java 21 开始
static void testStringOld(String response) {
    switch (response) {
        case null -> { }
        case String s -> {
            if (s.equalsIgnoreCase("YES"))
                System.out.println("你答对了");
            else if (s.equalsIgnoreCase("NO"))
                System.out.println("真遗憾");
            else
                System.out.println("什么?");
        }
    }
}

这里的问题是,使用单个模式来区分案例并不能扩展到单个条件之外。我们更希望编写多个模式,但随后我们需要某种方式来表达对模式的细化。因此,我们允许在 switch 块中使用 when 子句来为模式 case 标签指定守卫,例如 case String s when s.equalsIgnoreCase("YES")。我们将这样的 case 标签称为 守卫 case 标签,将布尔表达式称为 守卫

采用这种方法,我们可以使用守卫重写上述代码:

java
// 从 Java 21 开始
static void testStringNew(String response) {
    switch (response) {
        case null -> { }
        case String s
        when s.equalsIgnoreCase("YES") -> {
            System.out.println("你答对了");
        }
        case String s
        when s.equalsIgnoreCase("NO") -> {
            System.out.println("真遗憾");
        }
        case String s -> {
            System.out.println("什么?");
        }
    }
}

这导致了一种更易于阅读的 switch 编程风格,其中测试的复杂性出现在 switch 规则的左侧,如果满足该测试,则逻辑位于 switch 规则的右侧。

我们可以通过为其他已知常量字符串添加额外规则来进一步增强此示例:

java
// 从 Java 21 开始
static void testStringEnhanced(String response) {
    switch (response) {
        case null -> { }
        case "y", "Y" -> {
            System.out.println("你答对了");
        }
        case "n", "N" -> {
            System.out.println("真遗憾");
        }
        case String s
        when s.equalsIgnoreCase("YES") -> {
            System.out.println("你答对了");
        }
        case String s
        when s.equalsIgnoreCase("NO") -> {
            System.out.println("真遗憾");
        }
        case String s -> {
            System.out.println("什么?");
        }
    }
}

这些示例展示了 case 常量、case 模式和 null 标签如何结合使用,以展示 switch 编程的新功能:我们可以将过去与业务逻辑混合在一起的复杂条件逻辑简化为一个可读的、顺序排列的 switch 标签列表,业务逻辑位于 switch 规则的右侧。

开关和枚举常量

目前,在 case 标签中使用枚举常量受到了很大的限制:switch 的选择器表达式必须是枚举类型,并且标签必须是枚举常量的简单名称。例如:

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

即使在添加了模式标签之后,这一限制也会导致代码过于冗长。例如:

java
// 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 常量使用枚举常量的限定名称。这样,上面的代码就可以改写为:

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

现在,我们为每个枚举常量提供了一个直接的 case,而无需使用受保护的类型模式,这些模式之前仅用于解决类型系统当前限制的问题。

描述

我们通过四种方式增强了 switch 语句和表达式:

  • 改进枚举常量 case 标签,

  • 扩展 case 标签以包括除常量以外的模式和 null

  • 扩大 switch 语句和 switch 表达式选择器表达式所允许的类型范围(以及随之而来的对 switch 块穷尽性所需的更丰富分析),以及

  • 允许在 case 标签后跟随可选的 when 子句。

改进的枚举常量 case 标签

长期以来,当在枚举类型上进行切换时,唯一有效的 case 常量是枚举常量。但这是一个强制性要求,随着新的、更丰富的 switch 形式的出现,这一要求变得沉重。

为了与现有的 Java 代码保持兼容,当在枚举类型上进行切换时,case 常量仍然可以使用正在切换的枚举类型常量的简单名称。

对于新代码,我们扩展了对枚举的处理。首先,我们允许枚举常量的限定名称作为 case 常量出现。这些限定名称可以在枚举类型上进行切换时使用。

其次,当使用枚举的一个常量的名称作为 case 常量时,我们取消了选择器表达式必须是枚举类型的要求。在这种情况下,我们要求名称是限定的,并且其值与选择器表达式的类型兼容。(这使枚举 case 常量的处理与数值 case 常量的处理保持一致。)

例如,允许以下两种方法:

java
// Java 21 起
sealed interface Currency permits Coin {}
enum Coin implements Currency { HEADS, TAILS }

static void goodEnumSwitch1(Currency c) {
    switch (c) {
        case Coin.HEADS -> {    // 枚举常量的限定名称作为标签
            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 -> {    // 不必要的限定但允许
            System.out.println("Tails");
        }
    }
}

以下示例是不允许的:

java
// Java 21 起
static void badEnumSwitch(Currency c) {
    switch (c) {
        case Coin.HEADS -> {
            System.out.println("Heads");
        }
        case TAILS -> {         // 错误 - TAILS 必须限定
            System.out.println("Tails");
        }
        default -> {
            System.out.println("Some currency");
        }
    }
}

switch 标签中的模式

我们修改了 switch 块中 switch 标签的语法规则(对比 JLS §14.11.1):

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

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

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

在成功进行模式匹配后,我们经常需要进一步测试匹配的结果。这可能会导致代码变得繁琐,例如:

java
// 从 Java 21 开始
static void testOld(Object obj) {
    switch (obj) {
        case String s:
            if (s.length() == 1) { ... }
            else { ... }
            break;
        ...
    }
}

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

为了解决这个问题,我们引入了 带守卫的模式 case 标签,允许在模式标签后面跟随一个可选的 守卫,守卫是一个布尔表达式。这允许将上述代码重写,以便将所有条件逻辑都提升到 switch 标签中:

java
// 从 Java 21 开始
static void testNew(Object obj) {
    switch (obj) {
        case String s when s.length() == 1 -> ...
        case String s                      -> ...
        ...
    }
}

第一个子句在 obj 既是 String又是 长度为 1 时匹配。第二个情况在 obj 是任意长度的 String 时匹配。

只有模式标签可以有守卫。例如,编写一个带有 case 常量和守卫的标签是无效的;例如,case "Hello" when callRandomBooleanExpression()

在支持 switch 中的模式时,需要考虑以下五个主要的语言设计领域:

  • 增强的类型检查
  • switch 表达式和语句的穷尽性
  • 模式变量声明的范围
  • 处理 null
  • 错误

增强的类型检查

选择器表达式类型

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

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

java
// 从 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 标签适用。例如,如果选择器表达式评估为 String,则 case 标签 case String scase CharSequence cs 都将适用。

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

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

java
// 从 Java 21 开始
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

只有当前者的模式支配后者的模式 并且 其守卫是一个值为 true 的常量表达式时,受保护的模式 case 标签才支配另一个模式 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 标签之前:

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

编译器会检查所有 case 标签。在 switch 块中,如果某个 case 标签被该 switch 块中前面的任何 case 标签支配,则会导致编译时错误。这种支配要求确保如果 switch 块仅包含类型模式 case 标签,则它们将按子类型顺序出现。

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

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

java
// 从 Java 21 开始
static void matchAll(String s) {
    switch(s) {
        case String t:
            System.out.println(t);
            break;
        default:
            System.out.println("Something else");  // 错误 - 被支配!
    }
}

static void matchAll2(String s) {
    switch(s) {
        case Object o:
            System.out.println("An Object");
            break;
        default:
            System.out.println("Something else");  // 错误 - 被支配!
    }
}

switch 表达式和语句的完备性

类型覆盖

switch 表达式要求选择器表达式的所有可能值都必须在 switch 块中得到处理;换句话说,它必须是 完备的。这维护了 switch 表达式成功求值后总是产生一个值的属性。

对于普通的 switch 表达式,这一属性通过一组直接附加在 switch 块上的额外条件来强制执行。

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

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

java
// 假设 Java 21
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
// 假设 Java 21
static int coverage(Object obj) {
    return switch (obj) {           // 错误 - 仍然不完备
        case String s  -> s.length();
        case Integer i -> i;
    };
}

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

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

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

实践中的完备性

非模式 switch 表达式中已经存在类型覆盖的概念。例如:

java
// 假设 Java 20
enum Color { RED, YELLOW, GREEN }

int numLetters = switch (color) {   // 错误 - 不完备!
    case RED -> 3;
    case GREEN -> 5;
}

这个基于枚举类的 switch 表达式是不完备的,因为预期的输入 YELLOW 没有被覆盖。正如预期的那样,添加一个 case 标签来处理 YELLOW 枚举常量就足以使 switch 完备:

java
// 假设 Java 20
int numLetters = switch (color) {   // 完备!
    case RED -> 3;
    case GREEN -> 5;
    case YELLOW -> 6;
}

以这种方式编写的 switch 是完备的,这有两个重要好处。

首先,如果我们已经处理了所有情况,那么编写一个可能只是抛出异常的 default 子句将会很繁琐:

java
int numLetters = switch (color) {
    case RED -> 3;
    case GREEN -> 5;
    case YELLOW -> 6;
    default -> throw new ArghThisIsIrritatingException(color.toString());
}

在这种情况下手动编写 default 子句不仅令人恼火,而且实际上是有害的,因为编译器可以在没有它的情况下更好地检查完备性。(同样适用于任何其他全匹配子句,如 defaultcase null, default 或无条件类型模式。)如果我们省略 default 子句,那么我们将在编译时发现是否忘记了 case 标签,而不是在运行时发现——甚至可能永远都发现不了。

更重要的是,如果后来有人在 Color 枚举中添加了另一个常量,会发生什么?如果我们有一个显式的全匹配子句,那么我们只会在运行时发现新的常量值。但是,如果我们编写 switch 以覆盖编译时已知的所有常量,并省略全匹配子句,那么在下一次重新编译包含 switch 的类时,我们将会发现这一更改。全匹配子句有将完备性错误掩盖起来的风险。

总之:在可能的情况下,没有全匹配子句的完备 switch 比有全匹配子句的完备 switch 更好。

从运行时间的角度来看,如果添加了一个新的 Color 常量,而包含 switch 的类没有被重新编译,会发生什么?新的常量有可能会暴露给我们的 switch。因为枚举总是存在这种风险,如果一个详尽的枚举 switch 没有包含匹配所有情况的子句,那么编译器将合成一个 default 子句来抛出异常。这保证了 switch 在没有选择任何一个子句的情况下无法正常完成。

“详尽性”的概念旨在在覆盖所有合理情况的同时,避免强制你编写可能污染甚至主导你的代码但实际价值不大的许多罕见边缘情况之间取得平衡。换句话说:“详尽性”是真正运行时间详尽性的编译时近似值。

完整性和密封类

如果选择器表达式的类型是密封类(JEP 409),则类型覆盖检查可以考虑密封类的 permits 子句来确定 switch 块是否完整。这有时可以消除对 default 子句的需求,正如上面所论证的,这是一种良好的做法。考虑以下具有三个允许的子类 ABCsealed 接口 S 的示例:

java
// 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 {}    // 隐式 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
// 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) {
        // 完整,因为没有可能的 A 的情况!
        case B<Integer> bi -> 42;
    };
}

I 的唯一允许的子类是 AB,但编译器可以检测到,由于选择器表达式的类型为 I<Integer>,且 A 的任何参数化都不是 I<Integer> 的子类型,因此 switch 块只需覆盖 B 类即可完整。

同样,完整性的概念是一种近似。由于独立编译,接口 I 的新实现可能会在运行时出现,因此编译器在这种情况下将插入一个抛出异常的合成 default 子句。

由于记录模式(JEP 440)可以嵌套,因此完整性的概念变得更加复杂。因此,完整性的概念必须反映这种潜在的递归结构。

完整性和兼容性

完整性的要求同时适用于模式 switch 表达式和模式 switch 语句。为了确保向后兼容性,所有现有的 switch 语句都将不变地编译。但是,如果 switch 语句使用了本 JEP 中描述的任何 switch 增强功能,则编译器将检查其是否完整。(Java 语言的未来编译器可能会对不完整的遗留 switch 语句发出警告。)

更具体地说,任何使用模式或 null 标签或选择器表达式不是遗留类型(charbyteshortintCharacterByteShortIntegerStringenum 类型)之一的 switch 语句都需要完整性。例如:

java
// 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 {}    // 隐式 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;
    };
}

模式变量声明的范围

模式变量JEP 394)是通过模式声明的局部变量。模式变量声明的范围是不寻常的,因为它是 流敏感的。为了回顾,请考虑以下示例,其中类型模式 String s 声明了模式变量 s

java
// 自 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 标签中出现的模式声明,并引入三条新规则:

  1. 在受保护的 case 标签的模式中出现的模式变量声明的范围包括保护条件,即 when 表达式。

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

  3. 在带有标签的 switch 语句组的 case 标签中出现的模式变量声明的范围包括语句组的块语句。禁止通过声明模式变量的 case 标签进行穿透。

以下示例展示了第一条规则的作用:

java
// 自 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

以下变体展示了第二条规则的作用:

java
// 自 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 语句组的示例:

java
// 自 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 标签。请考虑以下错误的示例:

java
// 自 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:                 // 编译时错误
            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
// 从 Java 21 开始
void testScope4(Object obj) {
    switch (obj) {
        case String s:
            System.out.println("A string: " + s);  // 这里 s 在作用域内!
        default:
            System.out.println("Done");            // 这里 s 不在作用域内
    }
}

处理 null

传统上,如果选择器表达式计算结果为 null,则 switch 会抛出 NullPointerException。这是广为人知的行为,我们不建议对现有 switch 代码进行更改。然而,对于模式匹配和 null 值,存在合理且不会引发异常的语义,因此在模式 switch 块中,我们可以以更常规的方式处理 null,同时保持与现有 switch 语义的兼容性。

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

  • 如果选择器表达式计算结果为 null,则任何 null 情况标签都被视为匹配。如果与该 switch 块没有关联这样的标签,则 switch 会像以前一样抛出 NullPointerException

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

例如,给定下面的声明,执行 nullMatch(null) 将打印 null! 而不是抛出 NullPointerException

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

没有 case null 标签的 switch 块被视为具有一个 case null 规则,其主体抛出 NullPointerException。换句话说,以下代码:

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

等价于:

java
// 从 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 中的不同之处在于,您可以直接在 switch 内部处理这种情况。如果您在 switch 块中看到一个 null 标签,则该标签将与 null 值匹配。如果您在 switch 块中没有看到 null 标签,则对 null 值进行 switch 操作将像之前一样抛出 NullPointerException。因此, switch 块中对 null 值的处理被规范化了。

null 情况与 default 结合起来是有意义的,也并不罕见。为此,我们允许 null 情况标签具有可选的 default;例如:

java
// 从 Java 21 开始
Object obj = ...
switch (obj) {
    ...
    case null, default ->
        System.out.println("其他情况(包括 null)");
}

如果 obj 的值是 null 引用值,或者没有其他 case 标签与之匹配,则该标签与该值匹配。

如果 switch 块同时包含一个带有 defaultnull``case 标签和一个 default 标签,则这是一个编译时错误。

错误

模式匹配可能会突然完成。例如,当将一个值与记录模式进行匹配时,记录的访问器方法可能会突然完成。在这种情况下,模式匹配被定义为通过抛出 MatchException 来突然完成。如果这样的模式作为 switch 中的一个标签出现,则 switch 也将通过抛出 MatchException 来突然完成。

如果 case 模式具有一个守卫,并且评估守卫会突然完成,则 switch 也会因相同原因而突然完成。

如果在模式 switch 中没有标签与选择器表达式的值匹配,则由于模式 switch 必须是穷尽的,因此 switch 会通过抛出 MatchException 来突然完成。

例如:

java
// 从 Java 21 开始
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。(一个总是抛出异常的记录访问器方法是非常不规则的,而一个抛出 MatchException 的穷尽模式 switch 也是极不寻常的。)

相比之下:

java
// 从 Java 21 开始
static void example(Object obj) {
    switch (obj) {
        case R r when (r.i / 0 == 1): System.out.println("它是 R!");
        default: break;
    }
}

调用 example(new R(42)) 会导致抛出 ArithmeticException

为了与模式 switch 语义保持一致,现在当没有 switch 标签在运行时适用时,对 enum 类的 switch 表达式将抛出 MatchException 而不是 IncompatibleClassChangeError。这是对语言的一个微小的不兼容更改。(一个对枚举的穷尽 switch 仅在 enum 类在 switch 编译后被更改时才会不匹配,这是非常不寻常的。)

未来工作

  • 目前,模式 switch 不支持基本类型 booleanlongfloatdouble。允许这些基本类型也将意味着允许它们在 instanceof 表达式中使用,并将基本类型模式与引用类型模式对齐,这将需要相当多的额外工作。这留给了未来可能的 JEP。

  • 我们预计,在将来,一般类将能够声明解构模式来指定它们如何与之匹配。这样的解构模式可以与模式 switch 一起使用,以生成非常简洁的代码。例如,如果我们有一个 Expr 的层次结构,其中包含 IntExpr(包含一个单独的 int)、AddExprMulExpr(包含两个 Expr),以及 NegExpr(包含一个单独的 Expr),我们可以与一个 Expr 匹配并在一个步骤中针对特定的子类型执行操作:

    java
    // 未来的 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 ep if e,甚至 p &&& e

  • 受保护模式标签的另一种替代方案是直接支持 受保护模式 作为一种特殊的模式形式,例如 p && e。在早期的预览中尝试过这种方法,但由此产生的布尔表达式歧义使我们更倾向于受保护的 case 标签而不是受保护的模式。

依赖项

本 JEP 建立在 JDK 16 中提供的 instanceof 的模式匹配JEP 394)以及 Switch 表达式JEP 361)提供的增强功能之上。它与记录模式JEP 440)共同发展。