Java 17 新特性 Pattern Matching for switch (Preview)

对官方文档(https://openjdk.org/jeps/406)的不完全翻译,有自己的理解,如有错误请指正。本文的概括总结请看https://blog.csdn.net/weixin_38833041/article/details/125450689

摘要

通过对 switch 表达式和语句的模式匹配以及对模式语言的扩展来增强 Java 编程语言。将模式匹配扩展到 switch 允许针对多个模式测试表达式,每个模式都有特定的操作,因此可以简洁安全地表达复杂的面向数据的查询。这是 JDK 17 中的预览语言特征。

目标

  • 通过允许模式出现在 case 标签中,扩展 switch 表达式和语句的表现力和适用性。
  • 允许在需要时放松 switch 对null的敌意。(允许选择器表达式的值为null)
  • 引入两种新的模式:保护模式,允许使用任意布尔表达式细化模式匹配逻辑,以及带括号的模式,以解决一些解析歧义。
  • 确保所有现有的 switch 表达式和语句不用更改就可以编译,并以相同的语义执行。
  • 不要引入与传统 switch 结构不同的具有模式匹配语义的新的类似 switch 的表达式或语句。
  • 不要让 switch 表达式或语句在 case 标签是模式时与 case 标签是传统常量时表现不同。

动机

在 Java 16 中,JEP 394 扩展了 instanceof 运算符以采用类型模式并执行模式匹配。这个适度的扩展允许简化熟悉的 instanceof-and-cast 习语:

// Old code
if (o instanceof String) {
    String s = (String)o;
    ... use s ...
}

// New code
if (o instanceof String s) {
    ... use s ...
}

我们经常希望将诸如 o 之类的变量与多个备选方案进行比较。自 Java 14 起(JEP 361)支持与 switch 语句的多路比较,但不幸的是 switch 非常有限。switch只能用于几种类型的值——数字类型、枚举类型和字符串——而且只能测试常量是否完全相等。我们可能希望使用模式来针对多种可能性测试相同的变量,对每种可能性采取特定的操作,但由于现有的switch不支持这一点,我们最终会得到一系列 if…else 测试,例如:

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 语句和表达式以适用于任何类型(switch不再局限于数字类型、枚举类型和String),case支持模式而不仅仅是常量,那么我们可以更清晰可靠地重写上面的代码:

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

这段代码的意图更清楚,因为我们使用了正确的控制结构:我们说,“参数 o 最多匹配以下条件之一,找出并评估相应分支。”作为奖励,它是可优化的;在这种情况下,我们更有可能在 O(1) 时间内执行调度。

pattern matching and null

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

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测试会带来不必要的样板文件和出错的机会(在switch外检查表达式的值是否为空)。最好将null测试放到switch中:

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

当选择器表达式的值为空时,switch的行为总是由它的case标签决定的。当有 case null 时(或总类型模式;参见下面的 4a),switch执行 case null 后面的代码;如果没有 case null,则 switch 会像以前一样抛出 NullPointerException。 (为了保持当前 switch 语义的向后兼容性,default不匹配null)

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

细化 switch 中的模式

对 switch 模式的实验表明,想要改进模式是很常见的。考虑以下切换 Shape 值的代码:

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 (如果这个case中没有break,会自动执行下一个case)来获得正确的行为。(注意在 if 块内小心放置 break;。)

这里的问题是,使用单一模式来区分案例不会超出单一条件(使用一个pattern无法区分多种情况,即模式case Triangle t无法区分大小三角形)。我们需要某种方式来表达对模式的改进。一种方法是细化case标签;这种细化在其他编程语言中称为守卫。例如,我们可以在 case 标签后引入一个新关键字 where ,where后面是一个布尔表达式,例如 case Triangle t where t.calculateArea() > 100。

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

使用这种方法,我们可以重新访问 testTriangle 代码来直接表达大三角形的特殊情况。这消除了在 switch 语句中使用 fall-through,这反过来意味着我们可以享受简洁的箭头样式 (->) 规则:

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) ,首先如果它匹配了类型模式 Triangle t,然后再匹配表达式 t.calculateArea() > 100 的计算结果为真。

当应用程序需求发生变化时,使用 switch 可以很容易地理解和更改case标签。例如,我们可能希望将三角形从default中拆分出来;我们可以通过使用细化模式和非细化模式来做到这一点:

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标签以包括除了常量之外的模式(不再局限于数字类型、枚举类型和String),以及
  • 引入两种新的模式:guarded patterns 和 parenthesized patterns(保护模式和括号模式)。

switch标签中的模式

该提案的核心是引入一个新的switch标签case p,其中 p 是一个模式。但是,开关的本质没有改变:选择器表达式的值与switch标签进行比较,选择其中一个标签,并执行与该标签关联的代码。现在的区别在于,对于带有模式的case标签,该选择是由模式匹配而不是由相等性检查确定的。例如,在以下代码中,o 的值与模式 Long l 匹配,将执行与 case Long l 关联的代码:

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的选择器表达式的类型必须是整型原始类型(char、byte、short 或 int)、相应的封装类型(Character、Byte、Short 或 Integer)、String 或枚举类型。我们对此进行了扩展,并要求选择器表达式的类型是整型原始类型或任何引用类型。

例如,在下面的模式切换中,选择器表达式 o 与涉及类、枚举类型、记录类型和数组类型的类型模式匹配(以及case null 和 default):

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标签,称为pattern label,我们使用现有的表达式与模式兼容的概念(JLS §14.30.1)。

1b.模式标签的主导地位

选择器表达式可以匹配 switch 块中的多个标签。考虑这个有问题的例子(bug:Label is dominated by a preceding case label 'CharSequence cs’):

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

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

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

case p 支配 case p && e 的,即,p && e是p的受保护版本(guarded version)。例如,模式标签 case String s 支配模式标签 case String s && s.length() > 0,因为每个匹配受保护模式(guarded pattern) String s && s.length() > 0 的值也匹配模式 String s。

编译器检查所有模式标签。如果 switch 块中的模式标签被该 switch 块中较早的模式标签支配,则这是一个编译时错误。

  • 这种支配性要求确保如果 switch 块仅包含类型模式case标签,这些将按子类型顺序出现。
  • 支配性的概念类似于 try 语句的 catch 子句的条件,如果捕获异常类 E 的 catch 子句前面有一个可以捕获 E 或 E 的超类的 catch 子句,则它是错误的(JLS § 11.2.3)。从逻辑上讲,前面的 catch 子句支配后面的 catch 子句。

如果一个switch块有多个全匹配(total pattern)标签(例如 case Object 和 default同时出现),这也是一个编译时错误。两个全匹配标签是default和总类型模式(参见下面的 4a)。

2. switch 表达式和语句中模式标签的完整性

switch 表达式要求选择器表达式的所有可能值都在 switch 块中处理。这保持了一个特性: switch 表达式的成功评估将始终产生一个值(switch’表达式必须涵盖所有可能的输入值)。对于普通的 switch 表达式,这是由 switch 块上一组相当简单的额外条件强制执行的。对于switch模式表达式,我们定义了switch块的类型覆盖的概念。

考虑这个(错误的)switch模式表达式:

static int coverage(Object o) {
    return switch (o) {         // Error - incomplete
        case String s -> s.length();
    };
}

switch 块只有一个 case 标签,case String s。这匹配类型为 String 子类型的选择器表达式的任何值。因此,我们说这个箭头规则的类型覆盖是 String 的每个子类型。此模式切换表达式不完整,因为其switch块的类型覆盖不包括选择器表达式的类型。

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

static int coverage(Object o) {
    return switch (o) {         // Error - incomplete
        case String s  -> s.length();
        case Integer i -> i;
    };
}

这个 switch 块的类型覆盖是它的两个箭头规则的覆盖的并集。换句话说,类型覆盖是String的所有子类型的集合和Integer的所有子类型的集合。但是,同样,类型覆盖仍然不包括选择器表达式的类型,所以这个switch模式表达式也是不完整的,并导致编译时错误。

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

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

如果选择器表达式的类型是密封类(JEP 409),那么类型覆盖检查可以考虑密封类的permits子句来确定switch块是否完整。考虑以下具有三个permits的子类 A、B 和 C 的密封接口 S 的示例:

sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {}  // Implicitly final

static int testSealedCoverage(S s) {
    return switch (s) {
        case A a -> 1;
        case B b -> 2;
        case C c -> 3;
    };
}

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

为了防止不兼容的单独编译,编译器会自动添加一个默认标签,其代码会引发 IncompatibleClassChangeError。只有更改了密封接口并且未重新编译switch代码时,才会运行到此标签。实际上,编译器会为您强化代码。

switch模式表达式的完整性要求类似于选择器表达式为枚举类的switch表达式的处理,如果枚举类的每个常量都有一个子句,则不需要default标签。

让编译器验证 switch 表达式是否完整非常有用。我们不仅将此检查仅用于 switch 表达式,还将其扩展到 switch 语句。出于向后兼容性的原因,所有现有的 switch 语句都将编译不变。但如果 switch 语句使用了本 JEP 中详述的任何新特性,那么编译器将检查它是否完整。

更准确地说,使用模式或null标签或其选择器表达式不是下面这些类型之一(char、byte、short、int、Character、Byte、Short、Integer、String 或 enum 类型)的 switch 语句需要保证完整性。

这意味着现在 switch 表达式和 switch 语句都可以获得更严格的类型检查的好处。例如:

sealed interface S permits A, B, C {}
final class A implements S {}
final class B implements S {}
record C(int i) implements S {}  // Implicitly final

static void switchStatementComplete(S s) {
    switch (s) {    // Error - incomplete; missing clause for permitted class B!
        case A a :
            System.out.println("A");
            break;
        case C c :
            System.out.println("B");
            break;
    };
}

保证大多数 switch 语句的完整性只需在 switch 主体的末尾添加一个简单的default子句。这使代码更清晰、更容易验证。例如,下面的 switch 语句是不完整的并且是错误的:

Object o = ...
switch (o) {    // Error - incomplete!
    case String s:
        System.out.println(s);
        break;
    case Integer i:
        System.out.println("Integer");
        break;
}

它可以被简单地完成:

Object o = ...
switch (o) {
    case String s:
        System.out.println(s);
        break;
    case Integer i:
        System.out.println("Integer");
        break;
    default:    // Now complete!
        break;
}

Java 语言的未来编译器可能会针对未完成的遗留 switch 语句发出警告。

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

模式变量 (JEP 394) 是由模式声明的局部变量。模式变量声明的不寻常之处在于它们的范围是流敏感的(flow-sensitive)。回顾一下下面的例子,其中类型模式 String s 声明了模式变量 s:

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 的 case 标签的模式变量声明的范围包括:出现在箭头右侧的表达式、块或 throw 语句。
  2. switch 的 case 标签(case后面没有其他case标签,可以有default标签)中的模式变量声明的范围包括:语句组的块语句。

补充(见4b):第一:case Character c -> {…}执行完这个case之后,不会继续执行下一个case,相当于这个case最后有一个默认的break。第二:case Character c: …执行完…会继续执行下一个case,除非显示的使用break。

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

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

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的声明范围包括语句组(该case标签中)的所有语句,即两个if语句和println语句。c的范围不包括default语句组的语句,即使执行完第一个语句组后会继续执行default标签中的语句。

必须排除通过声明模式变量的 case 标签的可能性作为编译时错误(有可能进入下一个case标签的模式变量的声明会导致编译错误)。考虑这个错误的例子:

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:                 // Compile-time error
            System.out.println("An integer " + i);
        default:
            break;
    }
}

如果这是允许的并且选择器表达式 o 的值是一个字符,那么 switch 块的执行可能会通过第二个语句组(在 case Integer i: 之后),其中模式变量 i 不会被初始化。因此,允许执行流程通过(fall through)声明模式变量的case标签是编译时错误。(在case Character c:最后一行加入break就不报错了)

这就是不允许 case Character c: case Integer i: … 的原因。类似的推理适用于在一个 case 标签中禁止多个模式:既不允许 case Character c, Integer i: … 也不允许 case Character c, Integer i -> …。如果允许这种标签,则 c 和 i 都将在冒号或箭头之后的范围内,但根据 o 的值是字符还是整数,只有 c 和 i 之一会被初始化。

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

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 代码更改它。

然而,鉴于模式匹配和空值存在合理且不包含异常的语义,有机会使switch模式对空值更加友好,同时保持与现有switch语义的兼容。

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

其次,我们观察到,如果选择器表达式的类型为total的模式出现了模式case标签,那么当选择器表达式的值为空时,该标签也将匹配。(比如case object也能匹配选择器表达式为null的情况)

如果 T 是 U 的子类型,则类型 U 的类型模式 p 是类型 T 的total。例如,类型模式 Object o 是类型String的total。

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

  • 如果选择器表达式的计算结果为 null,则任何 case null 标签或总模式标签(total pattern)都被认为是匹配的。如果没有与 switch 块相关联的标签,则 switch 会像以前一样抛出 NullPointerException。注意default不匹配null。
  • 如果选择器表达式的计算结果为非空值,那么我们会像往常一样选择匹配的 case 标签。如果没有 case 标签匹配,则任何全匹配(match-all label)标签(比如default)都被认为匹配。

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

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 增加了一个主体抛出 NullPointerException的 case null 。换句话说,这段代码:

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

相当于:

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 被抛出。

模式开关的不同之处在于您有一种机制可以在开关内部而不是外部直接处理null值。如果 switch 块中没有与 null 匹配的 case标签(不是没有case null会抛异常),则switch处理 null 值将像以前一样抛出NullPointerException。

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

JDK 16 中的 switch 块支持两种样式:一种基于标记的语句组(: 形式),其中可以fallthrough(没遇到break之前可以继续执行下一个case),另一种基于单一后项(single-consequent)形式(-> 形式),其中不可以fallthrough。在前一种风格中,多个标签通常写成 case l1: case l2: 而在后一种风格中,多个标签写成 case l1, l2:。

支持case null意味着许多特殊情况可以用 : 形式表示。例如:

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 -> label,如下所示:

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

o 的值与此标签匹配时,它要么是null,要么是字符串。在这两种情况下,模式变量 s 都使用 o 的值进行初始化。

(相反的形式,case String s,null 也应该被允许并且行为相同。)

将case null与default组合起来也很有意义(并且并不少见),即

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

同样,-> 形式也支持这种情况。为此,我们引入了一个新的例子:

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

不论o 的值是null,或者没有其他case匹配, o 的值都匹配此标签。

保护和括号模式(Guarded and parenthesized patterns)

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

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

不幸的是,所需的测试(即 o 是长度为 1 的字符串)在 case 标签和随后的 if 语句之间分开。如果模式切换支持 case 标签中模式和布尔表达式的组合,我们可以提高可读性。

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

如果 o 既是字符串且长度为 1,则第一种情况匹配。如果 o 是其他长度的字符串,则第二种情况匹配。

有时我们需要给模式加上括号以避免解析歧义。因此,我们扩展了模式语言以支持写成 § 的带括号的模式,其中 p 是一个模式。

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

Pattern:
  PrimaryPattern
  GuardedPattern

GuardedPattern:
  PrimaryPattern && ConditionalAndExpression

PrimaryPattern:
  TypePattern
  RecordPattern
  ArrayPattern
  ( Pattern )

受保护模式的形式为 p && e,其中 p 是模式,e 是布尔表达式。在受保护模式中,任何使用但未在子表达式中声明的局部变量、形式参数或异常参数必须是最终的或有效的最终的(effectively final)。

受保护的模式 p && e 引入了由模式 p 和表达式 e 引入的模式变量的并集。 p 中的任何模式变量声明的范围都包括表达式 e。这允许诸如 String s && (s.length() > 1) 之类的模式,它匹配可以强制转换为 String 的值,使得字符串的长度大于 1。

如果一个值匹配一个受保护的模式 p && e ,首先它匹配模式 p,其次表达式 e 的计算结果为真。如果该值与 p 不匹配,则不尝试计算表达式 e。

带括号的模式的形式为 §,其中 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 还消除了与具有受保护模式的案例标签有关的另一个潜在歧义。例如:

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

如果允许受保护模式的保护表达式是任意表达式,那么 -> 的第一次出现是 lambda 表达式的一部分还是 switch 规则的一部分(其主体是 lambda 表达式)将存在歧义。由于 lambda 表达式永远不可能是有效的布尔表达式,因此限制保护表达式的语法是安全的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值