Skip to content

JEP 432: Record Patterns (Second Preview) | 记录模式(第二次预览)

摘要

通过“记录模式”增强 Java 编程语言以解构记录值。记录模式和类型模式可以嵌套,以实现强大、声明式和可组合的数据导航和处理形式。这是一个 预览语言特性

历史

记录模式由 JEP 405 提议作为预览特性,并在 JDK 19 中交付。这个 JEP 提议进行第二次预览,基于持续的经验和反馈进行进一步的改进。

自第一次预览以来的主要变化是:

  • 增加对泛型记录模式的类型参数推断的支持。
  • 增加对记录模式出现在增强的 for 语句头部的支持。
  • 删除对命名记录模式的支持。

目标

  • 扩展模式匹配以表达更复杂、可组合的数据查询。

  • 不改变类型模式的语法或语义。

动机

在 JDK 16 中,JEP 394 扩展了 instanceof 运算符以接受一个“类型模式”并执行“模式匹配”。这个适度的扩展允许熟悉的 instanceof 和强制转换习惯用法被简化:

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

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

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

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

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

模式匹配和记录类

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

java
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
record Point(int x, int y) {}

void printSum(Object obj) {
    if (obj 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
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 s, String t),如果它是一个 Pair,并且递归地,其组件值匹配模式 String sString t。在我们上面的示例代码中,这些递归模式匹配失败,因为记录组件值都不是字符串,因此执行 else 块。

总之,嵌套模式消除了导航对象的意外复杂性,以便我们可以专注于这些对象所表达的数据。

instanceof 表达式和 switch 并不是唯一方便使用记录模式的分解行为的地方。在增强的 for 语句中允许记录模式将使遍历记录值的集合并快速提取每个记录的组件变得容易。例如:

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

static void dump(Point[] pointArray) {
    for (Point(var x, var y) : pointArray) {        // 头部的记录模式!
        System.out.println("(" + x + ", " + y + ")");
    }
}

其含义是直观的:在循环的每次迭代中,数组或 Iterable 的每个连续元素都与头部的记录模式进行模式匹配。

增强的 for 语句中的记录模式可以有嵌套模式,例如:

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

static void printUpperLeftColors(Rectangle[] r) {
    for (Rectangle(ColoredPoint(Point p, Color c), ColoredPoint lr): r) {
         System.out.println(c);
    }
}

描述

我们用可嵌套的记录模式扩展 Java 编程语言。

模式的语法将变为:

模式:
  类型模式
  括号模式
  记录模式

类型模式:
  局部变量声明

括号模式:
  ( 模式 )

记录模式:
  引用类型 记录结构模式

记录结构模式:
  ( [ 记录组件模式列表 ] )

记录组件模式列表:
  模式 {, 模式 }

记录模式

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

例如,给定声明:

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
record Box<T>(T t) {}

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

这里记录模式中的记录类类型是一个参数化类型。它可以等效地写成如下形式,在这种情况下类型参数将被推断:

java
static void test2(Box<String> bo) {
    if (bo instanceof Box(var s)) {    // 被推断为 Box<String>(var s)
        System.out.println("String " + s);
    }
}

推断适用于嵌套的记录模式。例如:

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

这里嵌套模式 Box(var s) 的类型参数被推断。如果也去掉外部记录模式中的类型参数,会更加简洁:

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

类型模式不支持类型参数的隐式推断;例如,类型模式 List l 总是被视为原始类型模式。

记录模式与详尽的 switch

JEP 420 增强了 switch 表达式和 switch 语句以支持包含模式(包括记录模式)的标签。switch 表达式和模式 switch 语句都必须是“详尽的”:switch 块必须有处理选择器表达式所有可能值的子句。对于模式标签,这是通过分析模式的类型来确定的;例如,case Bar b 这个标签匹配类型为 Bar 的值以及 Bar 的所有可能子类型。

对于涉及记录模式的模式标签,分析更加复杂,因为我们必须考虑组件模式的类型,并为“密封”层次结构留出余地。例如,考虑以下声明:

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
switch (p1) {                 // 错误!
    case Pair<A>(A a, B b) ->...
    case Pair<A>(B b, A a) ->...
}

这两个 switch 是详尽的,因为接口 I 是“密封的”,所以类型 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) ->...
}

记录模式与增强的 for 语句

如果 R 是一个记录模式,那么形式为

java
for (R : e) S

的增强 for 语句等价于下面这个增强 for 语句,它的头部没有记录模式:

java
for (var tmp : e) {
    switch(tmp) {
        case null -> throw new MatchException(new NullPointerException());
        case R -> S;
    }
}

这个转换有以下结果:

  • 记录模式 R 必须对数组或 Iterable 的元素类型“适用”。

  • 记录模式 R 对于数组或 Iterable 的元素类型必须是“详尽的”。

  • 如果 e 的任何元素是 null,那么增强 for 语句的执行将导致抛出 MatchException

例如:

java
record Pair(Object fst, Object snd){}

static void notApplicable(String[] arg) {
    for (Pair(var fst, var snd): arg) {   // 编译时错误,模式不适用
        System.out.println("An element");
    }
}

static void notExhaustive(Pair[] arg) {
    for (Pair(String s, String t): arg) { // 编译时错误,模式不详尽
        System.out.println(s+", "+t);
    }
}

static void exceptionTest() {
    Pair[] ps = new Pair[]{
        new Pair(1,2),
        null,
        new Pair("hello","world")
    };
    for (Pair(var f, var s): ps) {  // 运行时抛出 MatchException
        System.out.println(f);
    }
}

未来工作

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

  • 数组模式,其子模式匹配单个数组元素;
  • 可变参数模式,当记录是可变参数记录时;
  • 无关模式,它可以作为记录组件模式列表中的一个元素出现,但不声明模式变量;以及
  • 基于任意类而不仅仅是记录类的模式。

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

依赖关系

这个 JEP 建立在 JEP 394instanceof 的模式匹配)之上,在 JDK 16 中交付。