前言
相信你在Java编程中用到过如下的操作:
// 调用上游接口.返回结果obj
Object obj = getObj();
// 判断返回值是不是字符串
if (obj instanceof String) {
String objstr = (String) obj;
// do something with objstr
}
以上这种instanceof-and-cast 惯用语的代码基本每个人都写过。这段代码的含义是:
这里有三件事:检查(obj 是 String 吗?)、转换(将 obj 转换为 String)以及声明新的局部变量(objstr),
以便我们可以使用 string 值来操作后续的逻辑。
此模式简单明了,所有 Java 程序员都理解,但由于多种原因,此模式并不理想。这样写很无聊;
应该没有必要同时执行类型测试和强制转换,因为作为正常人来说,你在instanceof检查之后,你还会做除了类型转换的其他事情吗。
同时,特别是 String 类型的三次出现更是混淆了后面更重要的逻辑。上来啥也没干,就写了三个String了,难怪别人说java语言是最繁琐的语言。
但最重要的是,重复写同一个东西为错误提供了机会,这些错误会在不被注意的情况下悄无声息地潜入程序中。繁琐意味着容易出错。
基于以上的问题,java在jdk14中提出了模式匹配的语法机制来解决这个问题。该语法在JEP305中发布,作为第一次预览(preview)java模式匹配。
一、模式匹配的第一次预览
在jep305中发布的第一次模式匹配预览中,我们用他提供的语法来重写一下我们上面的例子。
// 调用上游接口.返回结果obj
Object obj = getObj();
// 判断返回值是不是字符串
if (obj instanceof String objstr) {
// do something with objstr
}
此时我们就看到,之前的1、检查 2、转换 3、创建新的变量 现在背缩减到了一步就完成了。编译通过,测试通过。
那么我们再把这个例子复杂化一点呢。我们增加一点逻辑。
public static void main(String[] args) {
// 调用上游接口
Object obj = getObj();
// 判断返回值是不是字符串
if (obj instanceof String objstr) {
// do something with objstr
System.out.println(objstr.toUpperCase());
}else {
// do something with other type
// 这里将无法使用objstr变量,编译报错
}
}
private static Object getObj() {
return "hello";
}
我们在这个例子中看到了绑定变量objstr的作用范围,他仅仅在if为true的分支可以使用,而在false的分支就超出了该变量的作用域。注意这句话,仅仅在你判断为true的时候可以使用。
而当我们修改一下if中的判断逻辑,简单的加一个取反符号。
public static void main(String[] args) {
// 调用上游接口
Object obj = getObj();
// 判断返回值是不是字符串
if (!(obj instanceof String objstr)) {
// do something with objstr
System.out.println(objstr.toUpperCase());// 这不可以使用objstr变量,编译报错
}else {
// do something with other type
// 这里可以使用objstr变量
System.out.println(objstr.toUpperCase());
}
}
我们看到我们就是加了一个取反符号,这个作用域就彻底反转过来了。所以他的变量范围只在true的if块中。当你的匹配表达式是false才能进入的分支的时候,该if中无法引用该变量。
而在得知了简单的作用域范围之后,我们再把if 语句的条件变得比单个 instanceof 更复杂时,他的对应的绑定变量的范围也会相应增加。例如,在此代码中:
public static void main(String[] args) {
// 调用上游接口
Object obj = getObj();
// 判断返回值是不是字符串
if (obj instanceof String objstr && objstr.length() > 2) {
System.out.println(objstr.toUpperCase());
}
}
private static Object getObj() {
return "hello";
}
我们把条件变成了obj instanceof String objstr && objstr.length() > 2 这种复合条件,此时你可以看到,他整体还是可用的,并不会有编译错误。
那么我们再改一下变成这样。
if (obj instanceof String objstr || objstr.length() > 2) {
System.out.println(objstr.toUpperCase());
}
我们仅仅是把条件中的&&变成了||,不好意思,此时报错了。
结合这两个案例,我们可以知道他的这种语法设计。那就是:
绑定变量 的作用域 位于 & & 运算符右侧的范围内,以及 true 块中。(仅当 instanceof 成功并分配给 s 时,才会对右侧进行评估,绑定变量 S 不在 ||运算符,也不在 true 块的范围内。
所以我们可以知道,他的作用是为了匹配类型,只有当他匹配类型成功之后,他才会为后面的分配作用域。
这个其实看着很晦涩,其实不难理解,我们还是最初那句话,因为作为正常人来说,你在instanceof检查之后,你还会做除了类型转换的其他事情吗。
所以我们一开始的出发点就是为了当你检查成功之后,我才会为你转换并且分配给一个变量中。
那么你检查不成功,我为啥还给你分配呢,直接就限制作用域得了。
所以我们来理解一下他的三种作用域范围。
第一种:if-else
// 调用上游接口
Object obj = getObj();
// 判断返回值是不是字符串
if (obj instanceof String objstr) {
// do something with objstr
System.out.println(objstr.toUpperCase());
}else {
// do something with other type
// 这里将无法使用objstr变量,编译报错
}
// 调用上游接口
Object obj = getObj();
// 判断返回值是不是字符串
if (!(obj instanceof String objstr)) {
// do something with objstr
System.out.println(objstr.toUpperCase());// 这不可以使用objstr变量,编译报错
}else {
// do something with other type
// 这里可以使用objstr变量
System.out.println(objstr.toUpperCase());
}
这种匹配下,我们还是说我们是为了你检查成功然后为你分配的,当你在检查之后为false才会进入的地方没必要给你匹配这个作用域。只有当你obj instanceof String objstr 检查成功了,我们才会为你分配作用域范围。
第二种:
if (obj instanceof String objstr && objstr.length() > 2) {
System.out.println(objstr.toUpperCase());
}
这种是没问题的,因为当你条件判断到objstr.length() > 2的时候,第一个条件obj instanceof String objstr已经必然为true了,所以符合我们的理解。
第三种:
if (obj instanceof String objstr || objstr.length() > 2) {
System.out.println(objstr.toUpperCase());
}
这种就不行了,因为当你条件判断到objstr.length() > 2的时候,无法确认第一个条件obj instanceof String objstr是不是正确,这种无法分配。
于是我们可以总结一句作用域的理解口诀,只有当模式匹配为true的地方,才能使用模式绑定的变量。
二、带来的效果
在 instanceof 中使用模式匹配应该会大大减少 Java 程序中显式强制转换的总数。此外,类型测试模式在编写相等方法时特别有用。以下取自 Effective Java 书籍第 10 章的相等方法:
@Override
public boolean equals(Object o) {
return (o instanceof CaseInsensitiveString) &&
((CaseInsensitiveString) o).s.equalsIgnoreCase(s);
}
此时你就能用模式匹配来优化一下了。我们可以改写为:
@Override
public boolean equals(Object o) {
return (o instanceof CaseInsensitiveString cis &&
cis.s.equalsIgnoreCase(s));
}
是不是简洁干净了一些,起码不是那么恶心了。
三、展望
你能看到他在第一次预览的时候2017/05/30,其实只是简单的实现了一些语法性质的东西,后面发展了很多。我们会以这个模式匹配为一个系列来逐渐看到在jdk23乃至jdk25的时候,他的一个进步。
而当我们回顾历史的时候,我们把时间线拖回2017年,彼时java感受到了来自其他语言的威胁,开始做出改变,他没有选择出一个折中的方案来实现,而是直接彻底解决这个问题。并且作为上帝视角来看,在今天jdk23刚发布,在看模式匹配的时候,你能看到其布局之深远。
但是比较搞笑的是在jdk14的第一次预览截止到jdk15发布期间,这个特性并没有收到太多来自社区的反馈。于是官方在jdk15发布的时候又创建了JEP375,JDK15登场,模式匹配的第二次预览。期待收到更多的社区反馈,来做出改进。