提到模式匹配(Pattern Matching),Java 开发人员可能会比较陌生。实际上,其他编程语言的开发人员早就已经使用过模式匹配了。JVM 上的编程语言 Scala 的模式匹配功能就很强大。
什么是模式匹配?
为了更好地解释模式匹配,我们从一个简单的例子开始。我们希望创建一个方法,可以把任何对象转换成 String 格式。这就需要根据对象的类型来进行不同的格式化操作。我们可以很容易就写出下面这样的代码。这段代码的核心是使用 instanceof 操作符来检查输入对象的类型,再根据对象类型进行格式化操作。
public class ObjectFormatter {public String format(Object input) {if (input == null) {return “”;} else if (input instanceof Number) {return NumberFormat.getNumberInstance().format(input);} else if (input instanceof LocalDateTime) {return ((LocalDateTime) input).format(DateTimeFormatter.ISO_DATE_TIME);} else {return input.toString();}}}
上述对 instanceof 操作符的使用就是模式匹配的一种简单形式。
一个模式由匹配 predicate 和模式变量的集合组成。
- 匹配 predicate 判断一个模式是否可以匹配目标对象。
- 如果模式匹配的话,模式变量的集合用来从目标对象中提取值。
在 instanceof 操作符的例子中,匹配 predicate 的作用是检查目标对象的类型,而模式变量的集合中只有一个变量,就是目标对象自身。这种类型的模式,被称为类型模式(type pattern)。除了类型模式之外,计划中的模式还包括记录类型模式和数组模式。
模式匹配是一个涵盖范围非常大的功能。根据现在 Java 的发布周期,模式匹配的内容会在不同的 Java 版本中逐渐添加进来。具体的发布周期可以参考下面的表格。这个表格的右侧三列表示的是不同的与模式匹配相关的功能,每一行表示这些功能在对应 Java 版本中的可用状态。
Java版本 | instanceof 模式 | switch 的模式匹配 | 记录类型模式 |
Java 14 | 预览 | ||
Java 15 | 二次预览 | ||
Java 16 | 正式功能 | ||
Java 17 | 正式功能 | 预览 | |
Java 18 | 正式功能 | 二次预览 | |
Java 19 | 正式功能 | 三次预览 | 预览 |
以 Java 17 为例,可以使用 instance 模式的正式功能,以及 switch 模式匹配的预览功能。
Java 18 和 Java 19 中可用的模式匹配功能也列在了表格中,作为参考。
instanceof 模式匹配
Java 中的 instanceof 操作符用来检查对象的类型。下面的代码给出了通常使用 instanceof 操作符的代码范式。在 if 语句中使用 instanceof 来进行检查,如果检查通过,则使用强制类型转换,把输入对象 obj 转换成 String 类型的 s,最后再使用变量 s。
if (obj instanceof String) {String s = (String) obj;}
从上述代码中可以看到,对 instanceof 操作符的使用范式是非常繁琐的,其中需要检查的目标类型 String 就出现了三次。在使用了 instanceof 模式匹配之后,代码可以简化很多。在下面的代码中, String s 表示类型模式,其中 String 是需要匹配的类型,s 是匹配成功之后用来捕获目标对象的变量。该变量 s 可以直接在 if 语句块中使用。
if (obj instanceof String s) {System.out.println(s.toUpperCase());}
模式变量使用的是流作用域(flow scoping)。一个模式变量能够出现在作用域中,当且仅当编译器可以推断出模式匹配必定成功,并且该变量被赋予了一个值时。在上面的例子中,if 语句块的代码只有在模式匹配成功了之后才会执行,变量 s 此时必定被赋予了值 obj,因此编译器可以确定 s 必定在 if 语句块的作用域中。
关于流作用域,其实不用了解太多。如果使用错误,编译器会提示你的。
下面的代码给出了 instanceof 模式匹配的代码示例。第一个 if 匹配 String 类型的同时,加上了对字符串长度的检查;第二个 if 匹配剩下的 String 类型的对象。在第一个 if 的条件中,obj instanceof String s 和 s.length() > 10 的顺序不能反过来。这里利用了 && 的短路(short-circuit)特性,当第一个 instanceof 模式匹配成功之后,才会执行后面的判断,这个时候 s 必然是一个 String 对象,可以安全地使用 length 方法;如果第一个 instanceof 模式不匹配,后面的判断不会被执行,因此也不会出现错误。
public class StringMatch {public void test(Object obj) {if (obj instanceof String s && s.length() > 10) {System.out.println(“长字符串 -> ” + s);} else if (obj instanceof String s) {System.out.println(“短字符串 -> ” + s);} else {System.out.println(“其他”);}}}
在 switch 语句和表达式中使用模式匹配
在 Java 17 中,switch 语句和表达式的 case 子句中可以使用模式匹配。该功能在 Java 17 中是预览功能,因此需要通过命令行参数 –enable-preview 来启用。switch 在很多时候可以替代嵌套的 if/else。
下面的代码使用 switch 语句加上模式匹配改写了上面的使用嵌套 if/else 的代码示例。使用 switch 比 if/else 更加简洁。这里的 switch 用的是箭头格式。
public class StringMatch {public void test(Object obj) {switch (obj) {case String s && s.length() > 10 -> System.out.println(“长字符串 -> ” + s);case String s -> System.out.println(“短字符串 -> ” + s);default -> System.out.println(“其他”);}}}
我们可以用 switch 语句改写文章开头提到的对象格式化的方法,如下面的代码所示。使用 switch 语句加上模式匹配的代码更加简洁易懂。
public class ObjectFormatter {public String format(Object input) {return switch (input) {case null -> “”;case Number n -> NumberFormat.getNumberInstance().format(n);case LocalDateTime t -> t.format(DateTimeFormatter.ISO_DATE_TIME);default -> input.toString();};}}