Skip to content

JEP 406: Pattern Matching for switch (Preview) | switch 表达式中的模式匹配(预览)

摘要

为 Java 编程语言中的 switch 表达式和语句引入模式匹配功能,并对模式语言进行扩展。将模式匹配扩展到 switch 允许表达式与多个模式进行匹配,每个模式对应一个特定的操作,从而使复杂的数据导向查询能够以简洁且安全的方式表达。这是 JDK 17 中的一项 预览语言特性

目标

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

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

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

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

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

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

动机

在 Java 16 中,JEP 394 扩展了 instanceof 操作符,使其可以接受一个 类型模式 并执行 模式匹配。这一小小的扩展允许简化熟悉的 instanceof- 和 -cast 惯用法:

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

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

我们经常需要将一个变量(如 o)与多个备选项进行比较。Java 通过 switch 语句支持多路比较,并且自 Java 14 起,switch 语句还引入了 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 分配值,那么我们就遇到了一个错误。(将 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 标签将与之匹配。(为了简洁起见,我们展示了一个 switch 表达式,但也可以展示一个 switch 语句;switch 块(包括 case 标签)将保持不变。)

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

模式匹配和 null

传统上,如果选择器表达式的值为 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 中会更好:

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 标签,然后在相应的语句组中不太自然地放置三角形面积的测试。然后,当三角形的面积小于 100 时,我们必须使用穿透(fall-through)来获得正确的行为。(注意在 if 块内仔细放置 break;。)

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

但是,有一种更具表现力的方法。我们不必扩展 case 标签的功能,而是可以扩展模式语言本身。我们可以添加一种新类型的模式,称为 守卫模式(guarded pattern),写为 p && b,允许模式 p 通过任意布尔表达式 b 进行细化。

使用这种方法,我们可以重新编写 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");
    }
}

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 " + Color.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

编译器会检查所有模式标签。如果在 switch 块中,一个模式标签被该块中前面的模式标签支配,则会出现编译时错误。

这种支配性要求确保,如果 switch 块仅包含类型模式情况标签,则这些标签将按子类型顺序出现。

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

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

2. 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 子类型的任何值。因此,我们说这个箭头规则的类型覆盖范围是 String 的所有子类型。这个模式 switch 表达式是不完整的,因为其 switch 块的类型覆盖范围不包括选择器表达式的类型。

考虑以下(仍然错误的)示例:

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

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 块是否完整。以下是一个包含三个允许的子类 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 testSealedCoverage(S s) {
    return switch (s) {
        case A a -> 1;
        case B b -> 2;
        case C c -> 3;
    };
}

编译器可以确定该 switch 块的类型覆盖范围是 ABC。由于选择器表达式的类型 S 是一个密封接口,其允许的子类恰好是 ABC,因此这个 switch 块是完整的。因此,不需要 default 标签。

为了防止不兼容的独立编译,编译器会自动添加一个 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 switchStatementComplete(S s) {
    switch (s) {    // 错误 - 不完整;缺少针对允许类 B 的子句!
        case A a :
            System.out.println("A");
            break;
        case C c :
            System.out.println("B");  // 注意:这里应该是打印"C",但重点在于完整性检查
            break;
    };
}

请注意,在上面的示例中,除了完整性检查之外,还有一个逻辑错误(即当 sC 的实例时,应该打印 "C" 而不是 "B")。然而,这里的重点是强调如果 switch 语句不完整(即缺少对允许类 B 的处理),则会导致编译错误。

使大多数 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)是由模式声明的局部变量。模式变量声明的范围具有 流敏感性(flow-sensitive),这是其独特之处。为了回顾,请考虑以下示例,其中类型模式 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");
    }
}

&& 表达式的右操作数以及“then”块中,s 的声明在作用域内。但是,在“else”块中它不在作用域内;为了控制转移到“else”块,模式匹配必须失败,在这种情况下,模式变量将不会被初始化。

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

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

  2. 在带有后续无 switch 标签的 switch 标记语句组的 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 语句中。

第二条规则更为复杂。首先,我们考虑一个 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 switch 标签并执行这些语句,但其范围并不包括 default 语句组的语句。

必须排除通过声明模式变量的 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 的值是 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

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

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

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

其次,我们观察到,如果针对选择器表达式的类型存在一个 完全 的模式,并且该模式出现在模式 case 标签中,那么当选择器表达式的值为 null 时,该标签也会匹配。

如果类型 T 是类型 U 的子类型,则类型 U 的类型模式 p 对于类型 T 是全面的。例如,类型模式 Object o 对于类型 String 是全面的。

我们取消了 switch 在遇到选择器表达式值为 null 时立即抛出 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 标签产生的新标签形式

JDK 16 中的 switch 块支持两种风格:一种基于标记的语句组(: 形式),其中可能发生穿透(fallthrough),另一种基于单结果形式(-> 形式),其中不可能发生穿透。在前一种风格中,多个标签通常写作 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("其他情况(包括 null)");
}

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

java
Object o = ...
switch(o) {
    ...
    case null, default ->
        System.out.println("其他情况(包括 null)");
}

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

带守卫和带括号的模式

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

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                      -> ...
        ...
    }
}

如果 o 既是 String 类型 长度为 1,则第一个情况匹配。如果 o 是其他长度的 String,则第二个情况匹配。

有时我们需要对模式加括号以避免解析歧义。因此,我们扩展了模式的语言以支持括号内的模式,其形式为 (p),其中 p 是一个模式。

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

java
Pattern:
  PrimaryPattern
  GuardedPattern

GuardedPattern:
  PrimaryPattern && ConditionalAndExpression

PrimaryPattern:
  TypePattern
  RecordPattern
  ArrayPattern
  ( 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

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

我们还修改了 instanceof 表达式的语法为:

java
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 一起使用,以生成非常简洁的代码。例如,如果我们有一个 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();
         };
    }

    如果没有这样的模式匹配,表达这种即席多态计算就需要使用繁琐的 访问者模式(visitor)。模式匹配通常更加透明和直接。

  • 添加 AND 和 OR 模式也可能很有用,以便为带有模式的 case 标签提供更多表达能力。

备选方案

  • 除了支持模式 switch 外,我们还可以定义一个 类型 switch,它仅支持根据选择器表达式的类型进行切换。这个功能在指定和实现上较为简单,但表达能力要差得多。

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

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

    java
    SwitchLabel:
      case Pattern [ when Expression ]
      ...

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

依赖关系

本 JEP 建立在 instanceof 的模式匹配(JEP 394)以及 switch 表达式提供的增强功能(JEP 361)之上。我们打算使本 JEP 与 JEP 405 同时进行,后者定义了两种支持嵌套的新模式类型。实现过程中可能会使用到动态常量(JEP 309)。