跳转到内容

JEP 394:instanceof的模式匹配

原文:https://openjdk.org/jeps/394
翻译:张欢

instanceof运算符的模式匹配来增强Java编程语言。模式匹配使程序中的通用逻辑,即从对象中有条件地提取组件,得以更简洁、更安全地表示。

历史

instanceof的模式匹配由JEP 305提出,并在JDK 14中作为预览特性交付。它在JEP 375中被再次提出,并在JDK 15中交付以做第二次预览。

本JEP提出在JDK 16中完成该特性,并作出下列优化:

  • 撤销模式变量必须是隐式final的限制,以减少局部变量和模式变量之间的不对称性。
  • ST的子类型时,使用S类型的表达式与T类型的模式进行比较,将成为instanceof表达式的一个编译器错误。(这种instanceof表达式总是会成功的,所以没有意义。相反的情况,一个总是会失败的模式匹配,已经是编译错误了。)

可以根据进一步的反馈合并其他反馈。

动机

几乎每个程序都包含某种逻辑,结合了对表达式的类型或结构的测试,然后有条件地提取其中的状态组件以进行进一步处理。例如,所有Java程序员都熟悉“先instanceof再转换”的习惯用法:

if (obj instanceof String) {
String s = (String) obj;
// 使用s
}

这里发生了三件事情:一个测试(obj是不是一个String),一个转换(将obj转换为String),和定义一个新的局部变量(s)以便我们可以使用字符串的值。这种模式很简单,并且所有Java程序员都可以理解,但是由于一些原因,这不是最优的。这很乏味:应该没有必要既做类型测试,同时又做类型转换(你还能在instanceof测试之后做什么其他的呢?)。这些样板代码——特别是出现了三次的String类型——混淆了后面更重要的逻辑。但最重要的是,重复代码为错误提供了机会且不易被察觉。

与寻求特定的解决方案相比,我们相信是时候让Java拥抱模式匹配了。模式匹配允许简洁地表达对象所需的“形态”(模式),并允许各种语句和表达式针对其输入来测试“形态”(匹配)。从Hashkell到C#,许多语言都出于其简洁性和安全性而拥抱了模式匹配。

描述

模式是二者的组合:(1)一个可以应用于目标的谓词条件或测试;(2)一些局部变量,即当谓词条件满足时由目标解构出来的模式变量

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

instanceof运算符(JLS 15.20.2)被扩展以支持类型模式,而不只是一个类型。

这允许我们将上述单调的代码重构成下面这样:

if (obj instanceof String s) {
// 让模式匹配做事情!
...
}

(这段代码中,词组String s就是类型模式。)含义很直观。instanceof运算符将目标obj匹配到该类型模式:如果obj是一个String的实例,那么将其转换为String并将值赋值到变量s上。

模式匹配的条件——如果值不能匹配到模式,那么模式变量不会被赋值——意味着我们需要谨慎考虑模式变量的作用域。我们可以做一些简单的事,让模式变量的作用域包含该语句和所有代码块中随后的语句。但不幸的是这样会污染结果,例如:

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

换句话说,第二个语句中模式变量p会处于被污染的状态——它在作用域中,但却不可访问,因为它没有被赋值。但尽管它不能被访问,但因为在作用域中,我们不能再次声明它。这意味着模式变量在声明之后被污染,所以开发者不得不为它们的模式变量考虑很多不重复的名字。

模式变量不是使用粗略近似作用域,而是使用流程作用域的概念。模式变量仅在编译器可以推断出模式明确会匹配并且变量将会被赋值的作用域内有效。这种分析是流程敏感的,并以现有的流程分析的工作方式类似,例如明确赋值。回到我们的例子:

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

这个理念是:“模式变量在它明确匹配的范围内”。这允许安全地重用模式变量,且既直观又熟悉,因为 Java 开发人员已经习惯于流程敏感的分析。

if语句的条件表达式变得比单个instanceof更加复杂时,模式变量的作用域也会对应地改变。例如,在这段代码中:

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

模式变量s的作用域包含&&运算符的右边,同时也是整个true的部分。(&&运算符的右边只会在模式匹配成功并赋值到s时才会执行。)另一方面,下面的代码无法编译:

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

因为||运算符的语义,模式变量s可能不会被赋值,所以流程分析检测到变量s的作用域不包括||的右侧。

instanceof中使用模式匹配,应该会显著减少在Java程序中使用显示转换的次数。类型测试模式在编写equals方法时特别有用。考虑下面来自《Effective Java》书中第10条的equals方法:

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

使用类型模式,意味着它可以重新写成更清楚的样子:

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

其他的equals方法更会有戏剧性地提升。考虑上述的Point类,我们可能会编写下面这样的equals方法:

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

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

public 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 在作用域内
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); // 字段被覆盖了
s = s + "\n"; // 为模式变量赋值
...
}
System.out.println(s); // 引用字段 s
...
}
}

然而,模式变量的流程作用域性质,意味着必须注意区分:名称是指覆盖了字段声明的模式变量,还是字段声明本身。

class Example2 {
Point p;
void test2(Object o) {
if (o instanceof Point p) {
// p 指的是模式变量
...
} else {
// p 指的是字段
...
}
}
}

instanceof的语法对应地扩展为:

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

未来的工作

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

备选方案 类型模式的收益,可以体现在if语句的流程类型类型switch结构中得到体现。模式匹配概括了这两种结构。