详解 Java 17 中的模式匹配(Pattern Matching)
提到模式匹配(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();
};
}
}