JEP 394:instanceof的模式匹配
原文:https://openjdk.org/jeps/394
翻译:张欢
用instanceof
运算符的模式匹配来增强Java编程语言。模式匹配使程序中的通用逻辑,即从对象中有条件地提取组件,得以更简洁、更安全地表示。
历史
instanceof
的模式匹配由JEP 305提出,并在JDK 14中作为预览特性交付。它在JEP 375中被再次提出,并在JDK 15中交付以做第二次预览。
本JEP提出在JDK 16中完成该特性,并作出下列优化:
- 撤销模式变量必须是隐式final的限制,以减少局部变量和模式变量之间的不对称性。
- 当S是T的子类型时,使用S类型的表达式与T类型的模式进行比较,将成为
instanceof
表达式的一个编译器错误。(这种instanceof
表达式总是会成功的,所以没有意义。相反的情况,一个总是会失败的模式匹配,已经是编译错误了。)
可以根据进一步的反馈合并其他反馈。
动机
几乎每个程序都包含某种逻辑,结合了对表达式的类型或结构的测试,然后有条件地提取其中的状态组件以进行进一步处理。例如,所有Java程序员都熟悉“先instanceof再转换”的习惯用法:
这里发生了三件事情:一个测试(obj
是不是一个String
),一个转换(将obj
转换为String
),和定义一个新的局部变量(s
)以便我们可以使用字符串的值。这种模式很简单,并且所有Java程序员都可以理解,但是由于一些原因,这不是最优的。这很乏味:应该没有必要既做类型测试,同时又做类型转换(你还能在instanceof
测试之后做什么其他的呢?)。这些样板代码——特别是出现了三次的String
类型——混淆了后面更重要的逻辑。但最重要的是,重复代码为错误提供了机会且不易被察觉。
与寻求特定的解决方案相比,我们相信是时候让Java拥抱模式匹配了。模式匹配允许简洁地表达对象所需的“形态”(模式),并允许各种语句和表达式针对其输入来测试“形态”(匹配)。从Hashkell到C#,许多语言都出于其简洁性和安全性而拥抱了模式匹配。
描述
模式是二者的组合:(1)一个可以应用于目标的谓词条件或测试;(2)一些局部变量,即当谓词条件满足时由目标解构出来的模式变量。
一个类型模式由指定类型的谓词和单个模式变量组成。
instanceof
运算符(JLS 15.20.2)被扩展以支持类型模式,而不只是一个类型。
这允许我们将上述单调的代码重构成下面这样:
(这段代码中,词组String s
就是类型模式。)含义很直观。instanceof
运算符将目标obj
匹配到该类型模式:如果obj
是一个String
的实例,那么将其转换为String
并将值赋值到变量s
上。
模式匹配的条件——如果值不能匹配到模式,那么模式变量不会被赋值——意味着我们需要谨慎考虑模式变量的作用域。我们可以做一些简单的事,让模式变量的作用域包含该语句和所有代码块中随后的语句。但不幸的是这样会污染结果,例如:
换句话说,第二个语句中模式变量p
会处于被污染的状态——它在作用域中,但却不可访问,因为它没有被赋值。但尽管它不能被访问,但因为在作用域中,我们不能再次声明它。这意味着模式变量在声明之后被污染,所以开发者不得不为它们的模式变量考虑很多不重复的名字。
模式变量不是使用粗略近似作用域,而是使用流程作用域的概念。模式变量仅在编译器可以推断出模式明确会匹配并且变量将会被赋值的作用域内有效。这种分析是流程敏感的,并以现有的流程分析的工作方式类似,例如明确赋值。回到我们的例子:
这个理念是:“模式变量在它明确匹配的范围内”。这允许安全地重用模式变量,且既直观又熟悉,因为 Java 开发人员已经习惯于流程敏感的分析。
当if
语句的条件表达式变得比单个instanceof
更加复杂时,模式变量的作用域也会对应地改变。例如,在这段代码中:
模式变量s
的作用域包含&&
运算符的右边,同时也是整个true的部分。(&&
运算符的右边只会在模式匹配成功并赋值到s
时才会执行。)另一方面,下面的代码无法编译:
因为||
运算符的语义,模式变量s
可能不会被赋值,所以流程分析检测到变量s
的作用域不包括||
的右侧。
在instanceof
中使用模式匹配,应该会显著减少在Java程序中使用显示转换的次数。类型测试模式在编写equals
方法时特别有用。考虑下面来自《Effective Java》书中第10条的equals
方法:
使用类型模式,意味着它可以重新写成更清楚的样子:
其他的equals
方法更会有戏剧性地提升。考虑上述的Point
类,我们可能会编写下面这样的equals
方法:
使用模式匹配作为替代,我们可以将多个语句组合为一个表达式,消除重复并简化流程控制:
模式变量的流程范围分析,对于语句是否正常完成很敏感。例如,考虑下面的方法:
该方法测试它的参数o
是不是一个String
,如果不是则抛出异常。只有当条件语句正常结束时,才有可能到达println
语句。因为条件语句所包含的语句永远无法正常完成,这只有在条件语句运算得到false
时才会发生,反过来意味着模式匹配已经成功。因此,模式变量s
的作用域可以安全地包含代码块中条件语句之后的语句。
模式变量只是局部变量的一个特例,除了它作用域的声明之外,在所有其他方面,模式变量都会被视为局部变量。特别是,这意味着:(1)它们可以被赋值;(2)它们可以覆盖字段声明。例如:
然而,模式变量的流程作用域性质,意味着必须注意区分:名称是指覆盖了字段声明的模式变量,还是字段声明本身。
instanceof
的语法对应地扩展为:
未来的工作
未来的JEP将通过更丰富的模式来增强Java编程语言,例如记录类的解构模式,以及其他语言结构的模式匹配,例如switch
表达式和语句。
备选方案
类型模式的收益,可以体现在if
语句的流程类型或类型switch结构中得到体现。模式匹配概括了这两种结构。