Skip to content

JEP 440: Record Patterns | 记录模式

摘要

通过 记录模式(record patterns)来扩展 Java 编程语言,以便解构记录值。记录模式和类型模式可以嵌套,从而实现一种强大、声明式和可组合的数据导航和处理形式。

历史

记录模式作为预览功能由 JEP 405 提出,并在 JDK 19 中交付,随后由 JEP 432 再次预览,并在 JDK 20 中交付。此功能与 switch 的模式匹配JEP 441)共同演进,两者之间有着密切的交互。本 JEP 提议根据持续的经验和反馈进一步完善此功能,以最终确定其形态。

除了少数编辑上的小改动外,自第二次预览以来的主要变化是移除了增强型 for 语句头部中出现的记录模式的支持。该功能可能会在未来的 JEP 中重新提出。

目标

  • 将模式匹配扩展到 record 类实例的解构,以启用更复杂的数据查询。

  • 添加嵌套模式,以启用更可组合的数据查询。

动机

在 Java 16 中,JEP 394 扩展了 instanceof 运算符,使其能够接受 类型模式 并执行 模式匹配。这一小小的扩展允许简化熟悉的 instanceof 和强制类型转换习惯用法,使其更加简洁且不易出错:

java
// Java 16 之前
if (obj instanceof String) {
    String s = (String)obj;
    ... 使用 s ...
}

// Java 16 起
if (obj instanceof String s) {
    ... 使用 s ...
}

在新代码中,如果运行时 obj 的值是 String 的实例,则 obj 与类型模式 String s 匹配。如果模式匹配,则 instanceof 表达式的值为 true,且模式变量 s 被初始化为 obj 强制转换为 String 后的值,然后可以在包含的代码块中使用该值。

类型模式一举消除了许多强制类型转换的情况。然而,这只是向更加声明式、以数据为中心的编程风格迈出的第一步。随着 Java 支持新的、更具表达力的数据建模方式,模式匹配可以通过使开发人员能够表达其模型的语义意图来简化对这些数据的使用。

模式匹配与记录

记录(JEP 395)是数据的透明载体。接收记录类实例的代码通常会使用内置组件访问器方法来提取数据,这些数据被称为 组件。例如,我们可以使用类型模式来测试一个值是否是记录类 Point 的实例,如果是,则从该值中提取 xy 组件:

java
// 自 Java 16 起
record Point(int x, int y) {}

static void printSum(Object obj) {
    if (obj instanceof Point p) {
        int x = p.x();
        int y = p.y();
        System.out.println(x+y);
    }
}

这里的模式变量 p 仅用于调用访问器方法 x()y(),它们返回组件 xy 的值。(在每个记录类中,其访问器方法和组件之间存在一一对应的关系。)如果模式不仅能够测试一个值是否是 Point 的实例,还能够直接从该值中提取 xy 组件,并代表我们调用访问器方法,那就更好了。换句话说:

java
// 自 Java 21 起
static void printSum(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println(x+y);
    }
}

Point(int x, int y) 是一个 记录模式。它将用于提取组件的局部变量声明提升到模式本身中,并在值与模式匹配时通过调用访问器方法来初始化这些变量。实际上,记录模式将记录实例分解为其组件。

嵌套记录模式

模式匹配的真正强大之处在于它能够优雅地扩展到匹配更复杂的对象图。例如,考虑以下声明:

java
// 自 Java 16 起
record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

我们已经看到,可以使用记录模式来提取对象的组件。如果我们想从上左点中提取颜色,可以写为:

java
// 自 Java 21 起
static void printUpperLeftColoredPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
         System.out.println(ul.c());
    }
}

ColoredPointul 本身也是一个记录值,我们可能想要进一步分解它。因此,记录模式支持 嵌套,允许记录组件进一步与嵌套模式进行匹配和分解。我们可以在记录模式内部嵌套另一个模式,并同时分解外部和内部记录:

java
// 自 Java 21 起
static void printColorOfUpperLeftPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
                               ColoredPoint lr)) {
        System.out.println(c);
    }
}

嵌套模式进一步允许我们使用清晰且简洁的代码来分解聚合,这与组合聚合的代码一样清晰简洁。例如,在创建矩形时,我们可能会将构造函数嵌套在单个表达式中:

java
// 自 Java 16 起
Rectangle r = new Rectangle(new ColoredPoint(new Point(x1, y1), c1),
                            new ColoredPoint(new Point(x2, y2), c2));

使用嵌套模式,我们可以使用与嵌套构造函数结构相呼应的代码来解构这样的矩形:

java
// 自 Java 21 起
static void printXCoordOfUpperLeftPointWithPatterns(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(Point(var x, var y), var c),
                               var lr)) {
        System.out.println("Upper-left corner: " + x);
    }
}

当然,嵌套模式也可能匹配失败:

java
// 自 Java 21 起
record Pair(Object x, Object y) {}

Pair p = new Pair(42, 42);

if (p instanceof Pair(String s, String t)) {
    System.out.println(s + ", " + t);
} else {
    System.out.println("Not a pair of strings");
}

这里的记录模式 Pair(String s, String t) 包含两个嵌套的类型模式,即 String sString t。如果某个值是 Pair,并且递归地,其组件值匹配类型模式 String sString t,则该值匹配模式 Pair(String s, String t)。在我们上面的示例代码中,由于记录组件值都不是字符串,因此这些递归模式匹配失败,因此执行 else 块。

总之,嵌套模式消除了导航对象的偶然复杂性,使我们能够专注于这些对象所表达的数据。它们还赋予我们集中处理错误的能力,因为如果一个值不匹配嵌套模式 P(Q),则可能是因为子模式之一或两者都不匹配。我们无需检查和处理每个单独的子模式匹配失败——要么整个模式匹配成功,要么不成功。

描述

我们通过嵌套记录模式扩展了 Java 编程语言。

模式的语法变为:

Pattern:
  TypePattern
  RecordPattern

TypePattern:
  LocalVariableDeclaration

RecordPattern:
  ReferenceType ( [ PatternList ] )

PatternList :
  Pattern { , Pattern }

记录模式

记录模式 由一个记录类类型和一个(可能为空的)模式列表组成,该列表用于与对应的记录组件值进行匹配。

例如,给定声明

java
record Point(int i, int j) {}

如果值 v 是记录类型 Point 的实例,则它匹配记录模式 Point(int i, int j);如果是这样,则模式变量 i 将被初始化为在值 v 上调用与 i 对应的访问器方法的结果,模式变量 j 将被初始化为在值 v 上调用与 j 对应的访问器方法的结果。(模式变量的名称不必与记录组件的名称相同;即,记录模式 Point(int x, int y) 的行为完全相同,只是模式变量 xy 被初始化。)

null 值不匹配任何记录模式。

记录模式可以使用 var 来与记录组件进行匹配,而无需声明组件的类型。在这种情况下,编译器会推断出由 var 模式引入的模式变量的类型。例如,模式 Point(var a, var b) 是模式 Point(int a, int b) 的简写形式。

记录模式声明的模式变量集包括模式列表中声明的所有模式变量。

如果表达式可以强制转换为模式中的记录类型而无需进行未检查的转换,则该表达式与记录模式兼容。

如果记录模式命名了一个泛型记录类但未给出类型参数(即,记录模式使用了原始类型),则始终会推断类型参数。例如:

java
// 从 Java 21 开始
record MyPair<S,T>(S fst, T snd){};

static void recordInference(MyPair<String, Integer> pair){
    switch (pair) {
        case MyPair(var f, var s) ->
            ... // 推断的记录模式 MyPair<String,Integer>(var f, var s)
        ...
    }
}

对于记录模式的类型参数推断,在所有支持记录模式的构造中均受支持,即 instanceof 表达式和 switch 语句及表达式。

推断机制适用于嵌套记录模式;例如:

java
// Java 21 及以后版本
record Box<T>(T t) {}

static void test1(Box<Box<String>> bbs) {
    if (bbs instanceof Box<Box<String>>(Box(var s))) {
        System.out.println("String " + s);
    }
}

在此,嵌套模式 Box(var s) 的类型参数被推断为 String,因此模式本身被推断为 Box<String>(var s)

实际上,也可以省略外部记录模式的类型参数,从而得到更简洁的代码:

java
// Java 21 及以后版本
static void test2(Box<Box<String>> bbs) {
    if (bbs instanceof Box(Box(var s))) {
        System.out.println("String " + s);
    }
}

在此,编译器将推断整个 instanceof 模式为 Box<Box<String>>(Box<String>(var s))

为了保持兼容性,类型模式不支持类型参数的隐式推断;例如,类型模式 List l 始终被视为原始类型模式。

记录模式和穷举 switch

JEP 441 增强了 switch 表达式和 switch 语句,以支持模式标签。switch 表达式和模式 switch 语句都必须是 穷举的switch 块必须包含处理选择器表达式所有可能值的子句。对于模式标签,这是通过分析模式的类型来确定的;例如,case Bar b 标签与类型为 Bar 以及 Bar 所有可能子类型的值匹配。

当模式标签涉及记录模式时,分析会更为复杂,因为我们必须考虑组件模式的类型,并允许 sealed 层次结构。例如,考虑以下声明:

java
class A {}
class B extends A {}
sealed interface I permits C, D {}
final class C implements I {}
final class D implements I {}
record Pair<T>(T x, T y) {}

Pair<A> p1;
Pair<I> p2;

以下 switch 不是穷举的,因为没有匹配包含两个类型均为 A 的值的对:

java
// Java 21 及以后版本
switch (p1) {                 // 错误!
    case Pair<A>(A a, B b) -> ...
    case Pair<A>(B b, A a) -> ...
}

以下两个 switch 是穷举的,因为接口 Isealed 的,所以类型 CD 覆盖了所有可能的实例:

java
// Java 21 及以后版本
switch (p2) {
    case Pair<I>(I i, C c) -> ...
    case Pair<I>(I i, D d) -> ...
}

switch (p2) {
    case Pair<I>(C c, I i) -> ...
    case Pair<I>(D d, C c) -> ...
    case Pair<I>(D d1, D d2) -> ...
}

相比之下,以下 switch 不是穷举的,因为没有匹配包含两个类型均为 D 的值的对:

java
// Java 21 及以后版本
switch (p2) {                        // 错误!
    case Pair<I>(C fst, D snd) -> ...
    case Pair<I>(D fst, C snd) -> ...
    case Pair<I>(I fst, C snd) -> ...
}

未来工作

这里描述的记录模式可以在许多方向上进行扩展:

  • 可变参数模式,用于可变参数个数的记录;
  • 未命名模式,它可以出现在记录模式列表中,匹配任何值但不声明模式变量;
  • 能够应用于任意类值的模式,而不仅仅是记录类。

我们可能会在未来的 JEP 中考虑其中的一些扩展。

依赖项

本 JEP 基于 JDK 16 中提供的 instanceof 的模式匹配JEP 394)构建。它与 switch 的模式匹配JEP 441)共同演进。