跳到主要内容

JEP 394:instanceof 的模式匹配

QWen Max 中英对照 JEP 394: Pattern Matching for instanceof

总结

通过为 instanceof 操作符添加模式匹配来增强 Java 编程语言。模式匹配 能够以更简洁、更安全的方式表达程序中的常见逻辑,即从对象中条件性地提取组件。

历史

JEP 305 提出了 instanceof 的模式匹配,并在 JDK 14 中作为 预览功能 提供。之后,JEP 375 再次提出该特性,并在 JDK 15 中进行了第二轮预览。

本 JEP 提议在 JDK 16 中完成该功能,并进行以下改进:

  • 取消模式变量隐式为 final 的限制,以减少局部变量和模式变量之间的不对称性。

  • 如果模式 instanceof 表达式将类型为 S 的表达式与类型为 T 的模式进行比较,并且 ST 的子类型,则应将其视为编译时错误。(这种 instanceof 表达式总是会成功,因此毫无意义。相反的情况,即模式匹配总是失败的情形,已经是编译时错误了。)

其他改进可能会根据进一步的反馈进行整合。

动机

几乎每个程序都包含某种逻辑,用于测试表达式是否具有某种类型或结构,然后有条件地提取其状态的组成部分以进行进一步处理。例如,所有 Java 程序员都熟悉 instanceof-and-cast 模式:

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

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

与其寻找特定解决方案,我们认为现在是 Java 拥抱模式匹配的时候了。模式匹配允许对象所需的“形状”被简洁地表达(即模式),并且允许各种语句和表达式针对输入测试这种“形状”(即匹配)。从 Haskell 到 C#,许多语言都因为其简洁性和安全性而采用了模式匹配。

描述

模式 是以下两者的组合:(1) 可应用于目标的 谓词 或测试,以及 (2) 一组局部变量,称为 模式变量,只有当谓词成功应用于目标时,才会从目标中提取这些变量。

类型模式 由一个指定类型的谓词以及单个模式变量组成。

instanceof 运算符(JLS 15.20.2)被扩展为接受类型模式而不仅仅是类型。

这使得我们可以将上面繁琐的代码重构为以下内容:

if (obj instanceof String s) {
// Let pattern matching do the work!
...
}

(在此代码中,短语 String s 是类型模式。)其含义很直观。instanceof 运算符将目标 obj 与类型模式进行匹配,具体如下:如果 objString 的一个实例,则将其转换为 String 并将值赋给变量 s

模式匹配的条件性——如果一个值与模式不匹配,那么模式变量就不会被赋值——这意味着我们必须仔细考虑模式变量的作用域。我们可以采取一些简单的做法,比如说模式变量的作用域是包含它的语句以及封闭代码块中的所有后续语句。但这样会产生不幸的污染后果,例如:

if (a instanceof Point p) {
...
}
if (b instanceof Point p) { // ERROR - p is in scope
...
}

换句话说,根据第二条语句,模式变量 p 将处于一种“被毒化”的状态 —— 它在作用域内,但由于可能未被赋值,因此不应该被访问。然而,尽管它不应该被访问,但由于它在作用域中,我们无法简单地重新声明它。这意味着一个模式变量在声明后可能会变为“被毒化”状态,因此程序员需要为他们的模式变量想出许多不同的名称。

模式变量的作用域并非使用粗略的近似值,而是采用流作用域的概念。模式变量仅在编译器能够推断出模式一定匹配且变量已被赋值的情况下才处于作用域内。这种分析对流敏感,并且其工作方式与现有的流分析(如确定性赋值)类似。回到我们的示例:

if (a instanceof Point p) {
// p is in scope
...
}
// p not in scope here
if (b instanceof Point p) { // Sure!
...

}

这个口号是:“模式变量在确定匹配的地方是有效的”。这允许安全地重用模式变量,并且既直观又熟悉,因为 Java 开发人员已经习惯了流敏感分析。

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

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

模式变量 s&& 运算符的右侧以及真块(true block)中都是有效的。(只有当模式匹配成功并为 s 赋值时,&& 运算符的右侧才会被求值。)另一方面,以下代码无法编译:

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

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

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

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

使用类型模式意味着可以将其重写为更清晰的形式:

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

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

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

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

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

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

public void onlyForStrings(Object o) throws MyException {
if (!(o instanceof String s))
throw new MyException();
// s is in scope
System.out.println(s);
...
}

此方法测试其参数 o 是否为 String,如果不是则抛出异常。只有在条件语句正常完成的情况下,才能执行到 println 语句。由于条件语句的包含语句永远无法正常完成,所以这只能发生在条件表达式评估为值 false 的情况下,而这又意味着模式匹配已成功。因此,模式变量 s 的作用域安全地包含了方法块中条件语句之后的语句。

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

class Example1 {
String s;

void test1(Object o) {
if (o instanceof String s) {
System.out.println(s); // Field s is shadowed
s = s + "\n"; // Assignment to pattern variable
...
}
System.out.println(s); // Refers to field s
...
}
}

然而,模式变量的流作用域特性意味着需要谨慎判断一个名称是指模式变量声明遮蔽了字段声明,还是指字段声明本身。

class Example2 {
Point p;

void test2(Object o) {
if (o instanceof Point p) {
// p refers to the pattern variable
...
} else {
// p refers to the field
...
}
}
}

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

关系表达式:
     ...
     关系表达式 instanceof 引用类型
     关系表达式 instanceof 模式

模式:
     引用类型 标识符

未来工作

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

替代方案

类型模式的好处可以通过 if 语句中的流类型类型开关构造获得。模式匹配概括了这两种结构。