Skip to content
微信扫码关注公众号

JEP 405: Record Patterns (Preview) | 记录模式(预览)

摘要

通过引入记录模式(record patterns)来增强 Java 编程语言,以便解构记录值。记录模式和类型模式可以嵌套,从而实现一种强大、声明式且可组合的数据导航和处理形式。这是一个 预览语言特性

目标

  • 扩展模式匹配,以表达更复杂、可组合的数据查询。
  • 不改变类型模式的语法或语义。

动机

在 JDK 16 中,JEP 394 扩展了 instanceof 运算符,使其能够接收一个 类型模式 并执行 模式匹配。这一适度的扩展允许简化常见的 instanceof-and-cast 惯用法:

java
// 旧代码
if (o instanceof String) {
    String s = (String)o;
    ... 使用s ...
}

// 新代码
if (o instanceof String s) {
    ... 使用s ...
}

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

在 JDK 17 和 JDK 18 中,我们通过 JEP 406JEP 420 将类型模式的使用扩展到了 switchcase 标签。

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

模式匹配和记录类

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

java
record Point(int x, int y) {}

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

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

java
record Point(int x, int y) {}

void printSum(Object o) {
    if (o instanceof Point(int x, int y)) {
        System.out.println(x+y);
    }
}

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

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

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

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

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

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

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

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

java
Rectangle r = new Rectangle(new ColoredPoint(new Point(x1, y1), c1),
                            new ColoredPoint(new Point(x2, y2), c2));

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

java
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 编程语言,增加了可嵌套的记录模式。

模式的语法将变为:

Pattern:
  TypePattern
  ParenthesizedPattern
  RecordPattern

TypePattern:
  LocalVariableDeclaration

ParenthesizedPattern:
  ( Pattern )

RecordPattern:
  ReferenceType RecordStructurePattern [ Identifier ]

RecordStructurePattern:
  ( [ RecordComponentPatternList ] )

RecordComponentPatternList :
  Pattern { , Pattern }

记录模式

记录模式 由类型、一个(可能为空)的记录组件模式列表(用于与相应的记录组件进行匹配)以及一个可选的标识符组成。带有标识符的记录模式称为 命名 记录模式,该变量称为 记录模式变量

例如,给定声明

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

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

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

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

由记录模式声明的模式变量集合包括在记录组件模式列表中声明的所有模式变量,如果记录模式是命名记录模式,则还包括记录模式变量。

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

如果记录类是泛型的,那么任何命名此记录类的记录模式都必须使用泛型类型。例如,给定声明:

java
record Box<T>(T t) {}

以下方法是正确的:

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

而以下两种方法都会导致编译时错误:

java
static void erroneousTest1(Box<Object> bo) {
    if (bo instanceof Box(var s)) {                 // 错误
        System.out.println("I'm a box");
    }
}
static void erroneousTest2(Box b) {
    if (b instanceof Box(var t)) {                  // 错误
        System.out.println("I'm a box");
    }
}

未来我们可能会扩展推理,以推断泛型记录模式的类型参数。

记录模式和详尽的 switch

JEP 420 增强了 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;

在处理包含 Pair<A>Pair<I>switch 语句时,我们需要确保 switch 块中的模式涵盖了所有可能的记录组件类型组合,同时考虑到 Isealed 子类型 CD。这种详尽性要求确保 switch 块能够处理所有可能的输入情况。

以下 switch 不是详尽的,因为没有为包含两个类型都为 A 的值的对找到匹配项:

java
switch (p1) {                 // 错误!
    case Pair<A>(A a, B b) -> ...
    case Pair<A>(B b, A a) -> ...
}

这两个 switch 是详尽的,因为接口 Isealed 的,所以类型 CD 涵盖了所有可能的实例:

java
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
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 中实现的 JEP 394instanceof 的模式匹配)之上。