使用模式匹配

 

介绍模式匹配

模式匹配是一个仍在开发中的功能。该功能的某些元素已作为 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 的出现。

使用正则表达式的模式匹配按以下方式工作

  1. 它匹配给定的模式flame 是此示例,并将其与文本匹配
  2. 然后它为您提供有关模式匹配位置的信息。

在接下来的教程中,您需要牢记三个概念

  1. 您需要匹配的内容;这称为匹配目标。这里它是十四行诗。
  2. 您匹配的内容;这称为模式。这里正则表达式 flame
  3. 匹配的结果;这里开始索引和结束索引。

这三个元素是模式匹配的基本元素。

 

用于 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());
    }
}

使用模式变量

编译器允许您在任何有意义的地方使用变量 sif 分支是第一个想到的范围。事实证明,您也可以在 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 标签

  1. 以下数字类型:byteshortcharintlong 不被接受)
  2. 相应的包装类型:ByteShortCharacterInteger
  3. 类型 String
  4. 枚举类型。

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 记录有两个组件:xy

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)。此模式声明了两个模式变量xy。如果 o 确实是 Point 类型,则创建这两个绑定变量,并通过调用 Point 记录的相应访问器进行初始化。这一点很重要,因为您可能在这些访问器中有一些防御性复制。

记录模式使用记录的名称(本例中为 Point)以及该记录每个组件的一个类型模式构建。因此,当您编写 o instanceof Point(int x, int y) 时,int xint 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 日


返回教程列表