使用模式匹配
介绍模式匹配
模式匹配是一个仍在开发中的功能。该功能的某些元素已作为 Java 语言的最终功能发布,一些已作为预览功能发布,而另一些仍在讨论中。
如果您想了解更多关于模式匹配的信息并提供反馈,则需要访问 Amber 项目页面。Amber 项目页面是与 Java 语言中的模式匹配相关的所有内容的一站式页面。
如果您不熟悉模式匹配,您首先想到的可能是正则表达式中的模式匹配。如果是这样,您可能想知道它与“用于 instanceof 的模式匹配”有什么关系?
正则表达式是一种模式匹配形式,它被创建来分析字符字符串。这是一个良好且易于理解的起点。
让我们编写以下代码。
String sonnet = "From fairest creatures we desire increase,\n" +
"That thereby beauty's rose might never die,\n" +
"But as the riper should by time decease\n" +
"His tender heir might bear his memory:\n" +
"But thou, contracted to thine own bright eyes,\n" +
"Feed'st thy light's flame with self-substantial fuel,\n" +
"Making a famine where abundance lies,\n" +
"Thyself thy foe, to thy sweet self too cruel.\n" +
"Thou that art now the world's fresh ornament,\n" +
"And only herald to the gaudy spring,\n" +
"Within thine own bud buriest thy content,\n" +
"And, tender churl, mak'st waste in niggardly.\n" +
"Pity the world, or else this glutton be,\n" +
"To eat the world's due, by the grave and thee.";
Pattern pattern = Pattern.compile("\\bflame\\b");
Matcher matcher = pattern.matcher(sonnet);
while (matcher.find()) {
String group = matcher.group();
int start = matcher.start();
int end = matcher.end();
System.out.println(group + " " + start + " " + end);
}
此代码将莎士比亚的第一首十四行诗作为文本。此文本使用正则表达式 \bflame\b
进行分析。此正则表达式以 \b
开头和结尾。此转义字符在正则表达式中具有特殊含义:它表示单词的开头或结尾。在本例中,这意味着此模式匹配单词 flame
。
您可以使用正则表达式做更多的事情。它超出了本教程的范围。如果您想了解更多关于正则表达式的知识,您可以查看 正则表达式 页面。
如果您运行此代码,它将打印以下内容
flame 233 238
此结果告诉您,在十四行诗中索引 233 和 238 之间有一个 flame
的出现。
使用正则表达式的模式匹配按以下方式工作
- 它匹配给定的模式;
flame
是此示例,并将其与文本匹配 - 然后它为您提供有关模式匹配位置的信息。
在接下来的教程中,您需要牢记三个概念
- 您需要匹配的内容;这称为匹配目标。这里它是十四行诗。
- 您匹配的内容;这称为模式。这里正则表达式
flame
。 - 匹配的结果;这里开始索引和结束索引。
这三个元素是模式匹配的基本元素。
用于 Instanceof 的模式匹配
使用 Instanceof 将任何对象匹配到类型
扩展模式匹配的方法有很多。我们首先介绍的是用于 instanceof 的模式匹配;它已作为 Java SE 16 中的最终功能发布。
让我们将上一节的示例扩展到 instanceof
用例。为此,让我们考虑以下示例。
public void print(Object o) {
if (o instanceof String s){
System.out.println("This is a String of length " + s.length());
} else {
System.out.println("This is not a String");
}
}
让我们描述我们在那里介绍的三个元素。
匹配目标是任何类型的任何对象。它是 instanceof
运算符的左侧操作数:o
。
模式是类型后跟变量声明。它是 instanceof
的右侧。类型可以是类、抽象类或接口。在本例中,它只是 String s
。
匹配的结果是对匹配目标的新引用。此引用被放入作为模式一部分声明的变量中,在本例中为 s
。如果匹配目标与模式匹配,则会创建它。此变量具有您匹配的类型。s
变量称为模式的模式变量。某些模式可能具有多个模式变量。
在我们的示例中,变量 o
是您需要匹配的元素;它是您的匹配目标。模式是 String s
声明。匹配的结果是与类型 String
一起声明的变量 s
。仅当 o
的类型为 String
时才会创建此变量。
这种特殊的语法,您可以在其中使用 instanceof
声明的类型定义变量,是 Java SE 16 中添加的新语法。
模式 String s
称为类型模式,因为它检查匹配目标的类型。请注意,由于类型 String
扩展了类型 CharSequence
,因此以下模式将匹配
public void print(Object o) {
if (o instanceof CharSequence cs) {
System.out.println("This is a CharSequence of length " + s.length());
}
}
使用模式变量
编译器允许您在任何有意义的地方使用变量 s
。if
分支是第一个想到的范围。事实证明,您也可以在 if
语句的某些部分使用此变量。
以下代码检查 object
是否是 String
类的实例,以及它是否是非空字符串。您可以看到它在 &&
后的布尔表达式中使用了变量 s
。这是完全合理的,因为您仅在第一部分为 true
时才评估布尔表达式的这一部分。在这种情况下,变量 s
将被创建。
public void print(Object o) {
if (o instanceof String s && !s.isEmpty()) {
int length = s.length();
System.out.println("This object is a non-empty string of length " + length);
} else {
System.out.println("This object is not a string.");
}
}
在某些情况下,您的代码会检查变量的实际类型,如果此类型不是您期望的类型,则您将跳过代码的其余部分。请考虑以下示例。
public void print(Object o) {
if (!(o instanceof String)) {
return;
}
String s = (String)o;
// do something with s
}
从 Java SE 16 开始,您可以使用这种方式编写此代码,利用用于 instanceof
的模式匹配
public void print(Object o) {
if (!(o instanceof String s)) {
return;
}
System.out.println("This is a String of length " + s.length());
}
只要您的代码从 if
分支离开方法,s
模式变量就在 if
语句之外可用:使用 return
或通过抛出异常。如果您的代码可以执行 if
分支并可以继续执行方法的其余部分,则不会创建模式变量。
在某些情况下,编译器可以判断匹配是否失败。让我们考虑以下示例
Double pi = Math.PI;
if (pi instanceof String s) {
// this will never be true!
}
编译器知道 String
类是最终的。因此,变量 pi
不可能为 String
类型。编译器将对此代码发出错误。
使用用于 Instanceof 的模式匹配编写更简洁的代码
在许多地方,使用此功能将使您的代码更具可读性。
让我们创建以下 Point
类,并使用 equals()
方法。这里省略了 hashCode()
方法。
public class Point {
private int x;
private int y;
public boolean equals(Object o) {
if (!(o instanceof Point)) {
return false;
}
Point point = (Point) o;
return x == point.x && y == point.y;
}
// constructor, hashCode method and accessors have been omitted
}
这是编写 equals()
方法的经典方法;它可能是由 IDE 生成的。
您可以使用以下代码重写此 equals()
方法,该代码利用了用于 instanceof
功能的模式匹配,从而使代码更具可读性。
public boolean equals(Object o) {
return o instanceof Point point &&
x == point.x &&
y == point.y;
}
用于 Switch 的模式匹配
扩展 Switch 表达式以使用类型模式作为 Case 标签
用于 Switch 的模式匹配是 JDK 21 的最终功能。它在 Java SE 17、18、19 和 20 中作为预览功能呈现。
用于 Switch 的模式匹配使用 switch 语句或表达式。它允许您将匹配目标与多个模式同时匹配。到目前为止,模式是类型模式,就像用于 instanceof
的模式匹配一样。
在这种情况下,匹配的目标是 switch 的选择器表达式。此类功能中存在多个模式;switch 表达式的每个 case 本身都是一个类型模式,遵循上一节中描述的语法。
让我们考虑以下代码。
Object o = ...; // any object
String formatted = null;
if (o instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
formatted = String.format("double %f", d);
} else {
formatted = String.format("Object %s", o.toString());
}
您可以看到它包含三个类型模式,每个 if 语句一个。switch 的模式匹配允许以以下方式编写此代码。
Object o = ...; // any object
String formatter = switch(o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
default -> String.format("Object %s", o.toString());
};
switch 的模式匹配不仅使您的代码更易读,而且还使代码性能更高。评估 if-else-if 语句与该语句的分支数量成正比;分支数量加倍,评估时间也加倍。评估 switch 不依赖于 case 的数量。我们说 if 语句的时间复杂度为O(n),而 switch 语句的时间复杂度为O(1)。
到目前为止,它不是模式匹配本身的扩展;它是 switch 的一项新功能,它接受类型模式作为 case 标签。
在其当前版本中,switch 表达式接受以下内容作为 case 标签
switch 的模式匹配增加了使用类型模式作为 case 标签的可能性。
使用带保护的模式
在 instanceof
的模式匹配的情况下,您已经知道,如果匹配的目标与模式匹配,则创建的模式变量可以在包含 instanceof
的布尔表达式中使用,如下例所示。
Object object = ...; // any object
if (object instanceof String s && !s.isEmpty()) {
int length = s.length();
System.out.println("This object is a non-empty string of length " + length);
}
这在 if 语句中效果很好,因为语句的参数是布尔类型。在 switch 表达式中,case 标签不能是布尔类型。因此,您不能编写以下内容
Object o = ...; // any object
String formatter = switch(o) {
// !!! THIS DOES NOT COMPILE !!!
case String s && !s.isEmpty() -> String.format("Non-empty string %s", s);
case Object o -> String.format("Object %s", o.toString());
};
事实证明,switch 的模式匹配已扩展为允许在类型模式之后添加布尔表达式。此布尔表达式称为保护,生成的 case 标签称为带保护的 case 标签。您可以在 when
子句中添加此布尔表达式,语法如下。
Object o = ...; // any object
String formatter = switch(o) {
case String s when !s.isEmpty() -> String.format("Non-empty string %s", s);
default -> String.format("Object %s", o.toString());
};
此扩展的 case 标签称为带保护的 case 标签。表达式 String s when !s.isEmpty()
就是这样的带保护的 case 标签。它由类型模式和布尔表达式组成。
记录模式
记录是一种特殊的不可变类类型,写成这样,在 Java SE 16 中引入。您可以访问我们的 记录页面 了解有关此功能的更多信息。
记录模式是一种特殊的模式,在 Java SE 21 中发布为最终功能。它在 Java SE 19 和 20 中作为预览功能提供。记录基于组件构建,这些组件在记录声明的一部分中声明。在以下示例中,Point
记录有两个组件:x
和 y
。
public record Point(int x, int y) {}
此信息使称为记录解构的概念成为可能,在记录模式匹配中使用。以下代码是记录模式使用的第一个示例。
Object o = ...; // any object
if (o instanceof Point(int x, int y)) {
// do something with x and y
}
目标操作数仍然是 o
引用。它与记录模式匹配:Point(int x, int y)
。此模式声明了两个模式变量:x
和 y
。如果 o
确实是 Point
类型,则创建这两个绑定变量,并通过调用 Point
记录的相应访问器进行初始化。这一点很重要,因为您可能在这些访问器中有一些防御性复制。
记录模式使用记录的名称(本例中为 Point
)以及该记录每个组件的一个类型模式构建。因此,当您编写 o instanceof Point(int x, int y)
时,int x
和 int y
是类型模式,用于匹配 Point
记录的第一个和第二个组件。请注意,在这种情况下,您使用基本类型定义类型模式。
记录模式基于记录的规范构造函数的相同模型构建。即使您在给定记录中创建了除规范构造函数之外的其他构造函数,该记录的记录模式始终遵循规范构造函数的语法。因此,以下代码无法编译。
record Point(int x, int y) {
Point(int x) {
this(x, 0);
}
}
Object o = ...; // any object
// !!! THIS DOES NOT COMPILE !!!
if (o intanceof Point(int x)) {
}
记录模式支持类型推断。用于编写模式的组件类型可以使用 var
推断,也可以是您在记录中声明的实际类型的扩展。
由于每个组件的匹配实际上都是一个类型模式,因此您可以匹配作为记录组件实际类型的扩展的类型。如果您在模式中使用的类型不能是记录组件实际类型的扩展,那么您将收到编译器错误。
以下是一个第一个示例,您可以在其中要求编译器推断绑定变量的实际类型。
record Point(double x, double y) {}
Object o == ...; // any object
if (o instanceof Point(var x, var y)) {
// x and y are of type double
}
在以下示例中,您可以根据 Box
记录组件的类型进行切换。
record Box(Object o) {}
Object o = ...; // any object
switch (o) {
case Box(String s) -> System.out.println("Box contains the string: " + s);
case Box(Integer i) -> System.out.println("Box contains the integer: " + i);
default -> System.out.println("Box contains something else");
}
与 instanceof
一样,您不能检查不可能的类型。这里,类型 Integer
不能扩展类型 CharSequence
,从而导致编译器错误。
record Box(CharSequence o) {}
Object o = ...; // any object
switch (o) {
case Box(String s) -> System.out.println("Box contains the string: " + s);
// !!! THE FOLLOWING LINE DOES NOT COMPILE !!!
case Box(Integer i) -> System.out.println("Box contains the integer: " + i);
default -> System.out.println("Box contains something else");
}
记录模式不支持装箱或拆箱。因此,以下代码无效。
record Point(Integer x, Integer y) {}
Object o = ...; // any object
// !!! DOES NOT COMPILE !!!
if (o instanceof Point(int x, int y)) {
}
最后一点:记录模式支持嵌套,因此您可以编写以下代码。
record Point(double x, double y) {}
record Circle(Point center, double radius) {}
Object o = ...; // any object
if (o instanceof Circle(Point(var x, var y), var radius)) {
// Do something with x, y and radius
}
更多模式
模式匹配现在受 Java 语言的三个元素支持,作为最终功能或预览功能
instanceof
关键字,switch
语句和表达式,- 以及扩展的
for
循环。
它们都支持两种模式:类型模式和记录模式。
在不久的将来还会有更多内容。Java 语言的更多元素可能会被修改,并将添加更多类型的模式。此页面将更新以反映这些修改。
上次更新: 2022 年 12 月 21 日
返回教程列表