Skip to content
公众号 - 佳佳的博客

JEP 420: Pattern Matching for switch (Second Preview) | switch 的模式匹配(第二个预览版)

摘要

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

历史

switch 的模式匹配由 JEP 406 提出,并在 JDK 17 中作为 预览特性 提供。本 JEP 提议在 JDK 18 中对这一特性进行第二次预览,并根据经验和反馈进行了小幅改进。

自第一次预览以来的增强包括:

  • 主导性检查现在强制要求相同类型的守卫模式之前必须出现常量 case 标签,以提高可读性(见下面的 1b);

  • 在密封层次结构中,当允许的直接子类仅扩展(泛型)sealed 超类的实例化时,switch 块的详尽性检查现在更加精确(见下面的 2)。

目标

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

  • 在需要时放宽 switch 对历史空指针的敌意。

  • 引入两种新型模式:守卫模式,允许使用任意布尔表达式来细化模式匹配逻辑;以及 括号模式,以解决一些解析歧义。

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

  • 不引入与传统 switch 构造分开的具有模式匹配语义的新 switch-like 表达式或语句。

  • case 标签是模式而非传统常量时,不使 switch 表达式或语句的行为有所不同。

动机

在 Java 16 中,JEP 394 扩展了 instanceof 操作符,使其可以接受一个 类型模式 并执行 模式匹配。这一简单的扩展使得原本熟悉的 instanceof-and-cast 模式得以简化:

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

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

我们经常需要将一个变量(如 o)与多个备选项进行比较。Java 通过 switch 语句支持多路比较,并且自 Java 14 起,switch 表达式(JEP 361)也提供了这一功能,但遗憾的是,switch 非常受限。它只能对少数类型的值进行切换——数值类型、枚举类型和 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 的语义很清晰:如果选择器表达式 o 的值与某个带有模式的 case 标签匹配,则该 case 标签就匹配。(为了简洁起见,我们展示了一个 switch 表达式,但也可以展示一个 switch 语句; switch 块,包括 case 标签,将保持不变。)

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

模式匹配与 null

传统上,如果选择器表达式求值为 nullswitch 语句和表达式会抛出 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 中会更好:

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 null(或完全类型模式;请参阅 下面的 4a),则 switch 执行与该标签关联的代码;如果不存在 case null,则 switch 抛出 NullPointerException,就像以前一样。(为了与当前 switch 语义的向后兼容性保持一致,default 标签不会与 null 选择器匹配。)

我们可能希望以与另一个 case 标签相同的方式处理 null。例如,在以下代码中,case null, String s 将同时匹配 null 值和所有 String 值:

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

switch 中细化模式

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 时得到正确的行为。(注意在 if 块内谨慎放置 break; 语句。)

这里的问题是,使用单个模式来区分不同情况并不能扩展到多个条件。我们需要一种方式来表达对模式的“细化”。一种方法可能是允许 case 标签被细化;在其他编程语言中,这种细化被称为 “守卫”(guard)。例如,我们可以引入一个新关键字 where,它出现在 case 标签的末尾,后跟一个布尔表达式,如 case Triangle t where t.calculateArea() > 100

然而,还有另一种方法:不是扩展 case 标签的功能,而是扩展模式语言本身。我们可以添加一种名为 “守卫模式” 的新模式类型,写为 p && b,它允许通过任意布尔表达式 b 来细化模式 p

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

java
static void testTriangle(Shape s) {
    switch (s) {
        case Triangle t && (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,则它与模式 Triangle t && (t.calculateArea() > 100) 匹配。

使用 switch 语句可以很容易地理解和更改案例标签,以适应应用程序需求的变化。例如,如果我们想将三角形从默认路径中分离出来,我们可以同时使用细化的模式和非细化的模式来实现:

java
static void testTriangle(Shape s) {
    switch (s) {
        case Triangle t && (t.calculateArea() > 100) ->
            System.out.println("Large triangle");
        case Triangle t ->
            System.out.println("Small triangle");
        default ->
            System.out.println("Non-triangle");
    }
}

描述

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

  • case 标签扩展为除了常量外还包括模式,
  • 引入两种新的模式类型:守卫模式(guarded patterns)和 括号模式(parenthesized patterns)。

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

case 标签可以包含模式时,存在四个主要的设计问题:

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

1. 增强的类型检查

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

switch 中支持模式意味着我们可以放宽对当前选择器表达式类型的限制。目前,普通 switch 的选择器表达式的类型必须是整型基本类型(charbyteshortint)、相应的装箱形式(CharacterByteShortInteger)、String或枚举类型。我们对此进行了扩展,要求选择器表达式的类型必须是整型基本类型或任何引用类型。

例如,在以下模式 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 with " + c.values().length + " values");
        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. 模式标签的支配性

switch 块中,选择器表达式可能会匹配多个标签。考虑以下有问题的示例:

java
static void error(Object o) {
    switch(o) {
        case CharSequence cs ->
            System.out.println("A sequence of length " + cs.length());
        case String s ->    // 错误 - 此模式被前面的模式支配
            System.out.println("A string: " + s);
        default -> {
            break;
        }
    }
}

第一个模式标签 case CharSequence cs 支配 第二个模式标签 case String s,因为所有匹配 String s 模式的值也匹配 CharSequence cs 模式,但反之不然。这是因为第二个模式的类型 String 是第一个模式类型 CharSequence 的子类型。

p 是选择器表达式类型的完全模式时,形式为 case p 的模式标签支配 case null 标签。这是因为完全模式匹配所有值,包括 null

形式为 case p 的模式标签支配形式为 case p && e 的模式标签,即原始模式的受保护版本。例如,模式标签 case String s 支配模式标签 case String s && s.length() > 0,因为所有匹配受保护模式 String s && s.length() > 0 的值也匹配模式 String s

模式标签可以支配常量标签。例如,模式标签 case Integer i 支配常量标签 case 42,当 A 是枚举类 E 的枚举常量时,模式标签 case E e 支配常量标签 case A。如果包含标签是支配的,则受保护的模式标签也支配常量标签;我们不会检查保护表达式,因为这在一般情况下是不可判定的。因此,受保护的模式标签 case String s && s.length() > 1 按预期支配常量标签 case "hello";同样,case Integer i && i <> 0 也支配标签 case 0。这导致了一种简单且易于阅读的 case 标签排序方式,其中常量标签应出现在受保护的模式标签之前,受保护的模式标签应出现在非受保护的类型模式标签之前:

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

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

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

如果 switch 块有多个匹配所有情况的 switch 标签,也是编译时错误。这两个 匹配所有情况的 标签是 default 和完全类型模式(见下文 4a)。

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 块只有一个 case 标签,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;
    };
}

如果选择器表达式的类型是密封类(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 {}  // 隐式地是 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 标签。

当允许的直接子类仅实现(泛型)密封超类的某个实例化时,需要格外小心。例如:

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 类即可穷尽。

为了防止不兼容的独立编译,编译器会自动添加一个 default 标签,其代码会抛出 IncompatibleClassChangeError。只有在更改了密封接口且未重新编译 switch 代码时,才会执行到这个标签。实际上,编译器为您的代码增加了健壮性。

(要求模式 switch 表达式穷尽,这类似于选择器表达式是枚举类时的 switch 表达式的处理,如果枚举类的每个常量都有一个对应的子句,则不需要 default 子句。)

让编译器验证 switch 表达式是否穷尽是非常有用的。我们不仅仅将这种检查限制在 switch 表达式 上,还将其扩展到 switch 语句。为了确保向后兼容性,所有现有的 switch 语句都将保持不变地编译。但如果 switch 语句使用了本 JEP 中详述的任何新功能,则编译器将检查其是否穷尽。

更确切地说,任何使用模式或 null 标签的 switch 语句,或者其选择器表达式不是遗留类型(charbyteshortintCharacterByteShortIntegerString 或枚举类型)之一的 switch 语句,都需要穷尽性。

这意味着现在 switch 表达式和 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 {}  // 隐式地是 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;
}

(未来的 Java 语言编译器可能会对不是穷尽的遗留 switch 语句发出警告。)

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” 块,模式匹配必须失败,在这种情况下,模式变量将不会被初始化。

我们将模式变量声明的这种流敏感的范围概念扩展到 switch 规则 case 标签中出现的模式声明,并引入两条新规则:

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

  2. 在带标签的 switch 语句组的 case 标签中出现的模式变量声明的范围包括语句组的块语句。不应可能通过声明了模式变量的 case 标签“落入”下一情况。

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

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 of value " + i.intValue());
        default -> {
            break;
        }
    }
}

模式变量 c 的声明范围是第一个箭头右侧的块。

模式变量 i 的声明范围是第二个箭头右侧的 throw 语句。

第二条规则更为复杂。首先,我们考虑一个只有一个 case 标签的带标签 switch 语句组的例子:

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 标签“落入”下一个情况是一个编译时错误。

这就是为什么 case Character c: case Integer i: ... 是不被允许的。类似的推理也适用于禁止在 case 标签中使用多个模式:无论是 case Character c, Integer i: ... 还是 case Character c, Integer i -> ... 都是不允许的。如果允许这样的 case 标签,那么在冒号或箭头之后,ci 都将处于作用域内,但只有当 o 的值是 CharacterInteger 时,其中一个才会被初始化。

另一方面,通过不声明模式变量的标签“落入”是安全的,如下例所示:

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

4. 处理 null

4a. 匹配 null

传统上,如果选择器表达式的值为 null,则 switch 会抛出 NullPointerException。这是广为人知的行为,我们不建议对现有 switch 代码进行更改。

然而,鉴于模式匹配和 null 值之间存在合理且非异常承载的语义,因此有机会使模式 switch 更加友好于 null,同时保持与现有 switch 语义的兼容性。

首先,我们为 case 引入了一个新的 null 标签,当选择器表达式的值为 null 时,它会匹配。

其次,我们观察到,如果对于选择器表达式的类型而言是“完全”的模式出现在模式 case 标签中,那么当选择器表达式的值为 null 时,该标签也将匹配。(类型模式 p 的类型为 U,如果 TU 的子类型,则 p 对于类型 T 是“完全”的。例如,类型模式 Object o 对于类型 String 是“完全”的。)

我们取消了如果选择器表达式的值为 null,则 switch 立即抛出 NullPointerException 的通用规则。相反,我们检查 case 标签以确定 switch 的行为:

  • 如果选择器表达式的值为 null,则任何 null 情况标签或完全模式情况标签都被视为匹配。如果与 switch 块关联的标签中没有这样的标签,则 switch 会抛出 NullPointerException,如以前一样。

  • 如果选择器表达式的值不是 null,则我们像往常一样选择一个匹配的 case 标签。如果没有 case 标签匹配,则任何匹配所有标签的标签都被视为匹配。

例如,给定下面的声明,评估 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 匹配的 case 标签,那么对 null 值进行 switch 操作将像以前一样抛出 NullPointerException

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

Java 16 中的 switch 块支持两种风格:一种是基于带标签的语句组(: 形式),其中可能发生贯穿(fall-through),另一种是基于单结果形式(-> 形式),其中不可能发生贯穿。在前一种风格中,多个标签通常写为 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 类型时,该标签与 o 的值匹配。在这两种情况下,模式变量 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 引用值或没有其他标签与之匹配,则该标签与 o 的值匹配。

带守卫和括号的模式

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

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

不幸的是,期望的测试——即 o 是一个长度为 1 的 String —— 被分割在 case 标签和随后的 if 语句之间。如果 switch 模式支持在 case 标签中将模式和布尔表达式组合起来,那么我们可以提高代码的可读性。

与其添加另一个特殊的 case 标签,我们通过添加 守卫模式(written p && e)来增强模式语言。这允许将上述代码重写,使得所有条件逻辑都提升到 case 标签中:

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

第一个 caseo 既是 String 且长度为 1 时匹配。第二个 caseo 是长度为其他值的 String 时匹配。

有时我们需要给模式加上括号以避免解析歧义。因此,我们扩展了模式语言以支持用括号括起来的模式(written (p)),其中 p 是一个模式。

更具体地说,我们改变了模式的语法。假设 JEP 405 中记录的模式和数组模式被添加,那么模式的语法将变为:

Pattern:
  PrimaryPattern
  GuardedPattern

GuardedPattern:
  PrimaryPattern && ConditionalAndExpression

PrimaryPattern:
  TypePattern
  RecordPattern
  ArrayPattern
  ( Pattern )

守卫模式(Guarded Pattern)的形式为 p && e,其中 p 是一个模式,e 是一个布尔表达式。在守卫模式中,子表达式中使用的但未声明的任何局部变量、形式参数或异常参数必须是 final 或实际上的 final

守卫模式 p && e 引入了由模式 p 和表达式 e 引入的模式变量的并集。模式 p 中任何模式变量声明的作用域都包括表达式 e。这允许使用如 String s && (s.length() > 1) 的模式,该模式匹配可以转换为 String 的值,且字符串长度大于一。

一个值如果首先匹配模式 p,并且表达式 e 计算结果为 true,则它匹配守卫模式 p && e。如果值不匹配 p,则不会尝试评估表达式 e

括号模式(Parenthesized Pattern)的形式为 (p),其中 p 是一个模式。括号模式 (p) 引入了由子模式 p 引入的模式变量。如果值与模式 p 匹配,则它与括号模式 (p) 匹配。

我们还更改了 instanceof 表达式的语法规则为:

InstanceofExpression:
  RelationalExpression instanceof ReferenceType
  RelationalExpression instanceof PrimaryPattern

这一更改以及守卫模式语法规则中的非终结符 ConditionalAndExpression 确保了例如 e instanceof String s && s.length() > 1 的表达式仍然可以无歧义地解析为 (e instanceof String s) && (s.length() > 1)。如果尾随的 && 打算成为守卫模式的一部分,则整个模式应该用括号括起来,例如 e instanceof (String s && s.length() > 1)

在守卫模式的语法规则中使用非终结符 ConditionalAndExpression 还消除了与带有守卫模式的 case 标签相关的另一种潜在歧义。例如:

java
boolean b = true;
switch (o) {
    case String s && b -> s -> s;
}

如果守卫模式的守卫表达式允许是任意表达式,则会出现歧义,即第一个 -> 是 lambda 表达式的一部分还是 switch 规则(其主体是 lambda 表达式)的一部分。由于 lambda 表达式永远不能是有效的布尔表达式,因此限制守卫表达式的语法是安全的。

未来工作

  • 目前,switch 模式不支持基本类型 booleanfloatdouble。这些类型的使用看似微乎其微,但可以考虑添加对它们的支持。

  • 我们期待在未来,通用类能够声明解构模式来指定它们如何与 switch 模式进行匹配。这种解构模式可以与 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,我们可以定义一种 类型 switch,它仅支持根据选择器表达式的类型进行切换。此功能更简单,易于指定和实现,但表达力较差。

  • 对于受保护模式,有许多其他语法选项,如 p where ep when ep if e,甚至 p &&& e

  • 受保护模式的另一种替代方案是直接支持作为 case 标签特殊形式的 守卫

    SwitchLabel:
      case Pattern [ when Expression ]
      ...

    case 标签中支持守卫需要引入 when 作为新的上下文关键字,而受保护模式则不需要新的上下文关键字或运算符。受保护模式提供了更高的灵活性,因为受保护模式可以出现在其应用位置附近,而不是位于 switch 标签的末尾。

依赖项

本 JEP 基于 instanceof 的模式匹配(JEP 394)以及 switch 表达式提供的增强功能(JEP 361)。当 JEP 405(记录模式和数组模式)出现时,最终的实现将很可能使用动态常量(JEP 309)。