Skip to content

JEP 394: Pattern Matching for instanceof | instanceof 的模式匹配

摘要

为 Java 编程语言中的 instanceof 操作符引入 模式匹配 功能。模式匹配 允许程序中的通用逻辑,即从对象中条件性地提取组件,以更简洁和安全的方式表达。

历史

针对 instanceof 的模式匹配由 JEP 305 提出,并在 JDK 14 中作为 预览功能 提供。随后,JEP 375 重新提出了这一特性,并在 JDK 15 中进行了第二轮预览。

本 JEP 提议在 JDK 16 中完成此特性的最终确定,并进行以下改进:

  • 移除模式变量隐式最终(final)的限制,以减少局部变量和模式变量之间的不对称性。

  • 对于模式 instanceof 表达式,如果比较的是类型为 S 的表达式和类型为 T 的模式,其中 ST 的子类型,则将其视为编译时错误(因为此 instanceof 表达式将始终成功且毫无意义。相反的情况,即模式匹配将始终失败,已经是一个编译时错误)。

根据进一步的反馈,可能还会纳入其他改进。

动机

几乎每个程序都包含某种逻辑,该逻辑结合了测试表达式是否具有特定类型或结构,然后条件性地提取其状态组件以供进一步处理。例如,所有 Java 程序员都熟悉 instanceof 和类型转换的惯用法:

java
if (obj instanceof String) {
    String s = (String) obj;    // 唉...
    ...
}

这里发生了三件事:一个测试(obj 是否是 String 类型?)、一个转换(将 obj 转换为 String 类型),以及一个新局部变量的声明(s),以便我们可以使用字符串值。这种模式简单直接,所有 Java 程序员都能理解,但出于几个原因并非最优。它很繁琐;进行类型测试和类型转换应该是没必要的(在 instanceof 测试之后你还会做什么?)。这种样板代码——特别是 String 类型的三次出现——掩盖了随后更重要的逻辑。但最重要的是,这种重复为错误悄悄潜入程序提供了机会。

与其寻找临时解决方案,我们认为现在是时候让 Java 采用 模式匹配 了。模式匹配允许以简洁的方式表达对象所需的“形状”(即 模式),并使各种语句和表达式能够测试其输入是否符合该“形状”(即 匹配)。从 Haskell 到 C#,许多语言都因模式匹配的简洁性和安全性而采用了它。

描述

模式(pattern)是(1)一个可以应用于目标的 谓词(predicate)或测试,以及(2)一组局部变量(称为 模式变量)的组合,这些变量仅当谓词成功应用于目标时才从目标中提取。

类型模式(type pattern)由一个指定类型的谓词和一个单独的模式变量组成。

instanceof 操作符(JLS 15.20.2)被扩展为接受一个类型模式而不仅仅是一个类型。

这允许我们将上面繁琐的代码重构为以下形式:

java
if (obj instanceof String s) {
    // 让模式匹配来完成工作!
    ...
}

(在这段代码中,短语 String s 是类型模式。)其含义直观易懂。instanceof 操作符将目标 obj 与类型模式进行匹配,如下所示:如果 objString 的实例,则将其转换为 String 类型,并将值赋给变量 s

模式匹配的条件性——如果一个值不匹配某个模式,则模式变量不会被赋予值——意味着我们必须仔细考虑模式变量的作用域。我们可以采取简单的方法,声明模式变量的作用域是其所在的语句以及封闭块中的所有后续语句。但这会带来不幸的“污染”后果,例如:

java
if (a instanceof Point p) {
   ...
}
if (b instanceof Point p) {         // 错误 - p 在作用域内
   ...
}

换句话说,在第二个语句中,模式变量 p 将处于污染状态——它在作用域内,但由于可能未被赋值,因此不应被访问。但即使它不应被访问,由于它在作用域内,我们也不能再次声明它。这意味着模式变量在声明后可能会变得“污染”,因此程序员需要为他们的模式变量想出许多不同的名称。

模式变量不使用粗略的范围近似,而是使用 流作用域(flow scoping)的概念。模式变量仅在编译器可以推断出模式已确定匹配且变量已被赋予值的情况下才在作用域内。这种分析是流敏感的,并且以与现有的流分析(如 确定赋值)类似的方式工作。回到我们的例子:

java
if (a instanceof Point p) {
    // p 在作用域内
    ...
}
// p 在这里不在作用域内
if (b instanceof Point p) {     // 当然可以!
    ...
}

宗旨是:“模式变量在已确定匹配的情况下才在作用域内”。这允许安全地重用模式变量,并且既直观又熟悉,因为 Java 开发人员已经习惯了流敏感分析。

if 语句的条件表达式变得比单个 instanceof 更复杂时,模式变量的作用域也会相应增长。例如,在以下代码中:

java
if (obj instanceof String s && s.length() > 5) {
    flag = s.contains("jdk");
}

模式变量 s&& 运算符的右侧以及 if 语句的真块中都在作用域内。(只有当模式匹配成功且 s 被赋予值时,&& 运算符的右侧才会被评估。)另一方面,以下代码无法编译:

java
if (obj instanceof String s || s.length() > 5) {    // 错误!
    ...
}

由于 || 运算符的语义,模式变量 s 可能尚未被赋值,因此流分析规定 s|| 运算符的右侧不在作用域内。

instanceof 中使用模式匹配应该能够显著减少 Java 程序中显式类型转换的总数。类型测试模式在编写相等性方法时特别有用。考虑从 Effective Java 的第 10 项中提取的以下相等性方法:

java
public final boolean equals(Object o) {
    return (o instanceof CaseInsensitiveString) &&
        ((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}

使用类型模式,该方法可以改写为更清晰的形式:

java
public final boolean equals(Object o) {
    return (o instanceof CaseInsensitiveString cis) &&
        cis.s.equalsIgnoreCase(s);
}

其他 equals 方法的改进更为显著。考虑上面的 Point 类,我们可能会编写如下的 equals 方法:

java
public final boolean equals(Object o) {
    if (!(o instanceof Point))
        return false;
    Point other = (Point) o;
    return x == other.x
        && y == other.y;
}

使用模式匹配,我们可以将这些多条语句合并为一个表达式,从而消除重复并简化控制流:

java
public final boolean equals(Object o) {
    return (o instanceof Point other)
        && x == other.x
        && y == other.y;
}

模式变量的流作用域分析对语句能否 正常完成 的概念敏感。例如,考虑以下方法:

java
public void onlyForStrings(Object o) throws MyException {
    if (!(o instanceof String s))
        throw new MyException();
    // s 在作用域内
    System.out.println(s);
    ...
}

该方法检查其参数 o 是否为 String,如果不是,则抛出异常。只有当条件语句正常完成时,才有可能执行到 println 语句。因为条件语句的包含语句永远不会正常完成(它会抛出异常),所以这只能发生在条件表达式求值为 false 的情况下,而这也意味着模式匹配已经成功。因此,模式变量 s 的作用域安全地包括方法块中条件语句之后的语句。

模式变量只是局部变量的一种特殊情况,除了它们的作用域定义之外,在其他所有方面,模式变量都被视为局部变量。特别是,这意味着(1)它们可以被赋值,并且(2)它们可以遮蔽字段声明。例如:

java
class Example1 {
    String s;

    void test1(Object o) {
        if (o instanceof String s) {
            System.out.println(s);      // 字段 s 被遮蔽
            s = s + "\n";               // 对模式变量的赋值
            ...
        }
        System.out.println(s);          // 引用字段 s
        ...
    }
}

然而,模式变量的流作用域特性意味着在确定一个名称是指向遮蔽字段声明的模式变量声明,还是指向字段声明本身时,必须小心谨慎。

java
class Example2 {
    Point p;

    void test2(Object o) {
        if (o instanceof Point p) {
            // p 引用模式变量
            ...
        } else {
            // p 引用字段
            ...
        }
    }
}

相应地,instanceof 的语法 也进行了扩展:

RelationalExpression:
    ...
    RelationalExpression instanceof ReferenceType
    RelationalExpression instanceof Pattern

Pattern:
    ReferenceType Identifier

未来工作

未来的 JEP 将通过更丰富的模式形式来增强 Java 编程语言,例如为记录类提供解构模式,以及为其他语言构造(如 switch 表达式和语句)提供模式匹配。

替代方案

类型模式的优点可以通过 if 语句中的 流类型推断 或通过 类型切换 构造来实现。模式匹配概括了这两种构造。