跳到主要内容

JEP 432:记录模式(第二次预览)

QWen Max 中英对照 JEP 432: Record Patterns (Second Preview)

总结

通过 record 模式 增强 Java 编程语言,以解构 record 值。Record 模式和类型模式可以嵌套使用,从而实现一种强大、声明式且可组合的数据导航和处理方式。这是一个 预览语言功能

历史

JEP 405 提议将记录模式作为预览功能,并在 JDK 19 中交付。本 JEP 建议根据持续的经验和反馈,进行进一步的改进后推出第二次预览。

自第一个预览版以来的主要变化包括:

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

目标

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

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

动机

在 JDK 16 中,JEP 394instanceof 运算符进行了扩展,使其能够接受一个类型模式并执行模式匹配。这一适度的扩展简化了常见的 instanceof-然后-强制转换的习惯用法:

// Old code
if (obj instanceof String) {
String s = (String)obj;
... use s ...
}

// New code
if (obj instanceof String s) {
... use 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 组件:

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 组件,并自动为我们调用访问器方法,那就更好了。换句话说:

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) 是一种记录模式(record pattern)。它将提取组件的局部变量声明提升到模式本身中,并在值与模式匹配时通过调用访问器方法来初始化这些变量。实际上,记录模式将记录实例分解为其组成部分。

模式匹配的真正强大之处在于,它可以优雅地扩展以匹配更复杂的关系图。例如,请考虑以下声明:

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

我们已经看到,可以使用记录模式提取对象的组成部分。如果要从左上角的点提取颜色,可以这样写:

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

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

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

嵌套模式还允许我们进一步使用清晰简洁的代码来拆分聚合对象,就像我们编写代码将其组合起来一样。例如,如果我们正在创建一个矩形,我们很可能会将构造函数嵌套在一个表达式中:

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

使用嵌套模式,我们可以解构这样的矩形,代码可以反映嵌套构造函数的结构:

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);
}
}

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

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 块。

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

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

record Point(int x, int y) {}

static void dump(Point[] pointArray) {
for (Point(var x, var y) : pointArray) { // Record Pattern in header!
System.out.println("(" + x + ", " + y + ")");
}
}

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

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

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 编程语言。

模式的语法将变为:

Pattern:
TypePattern
ParenthesizedPattern
RecordPattern

TypePattern:
LocalVariableDeclaration

ParenthesizedPattern:
( Pattern )

RecordPattern:
ReferenceType RecordStructurePattern

RecordStructurePattern:
( [ RecordComponentPatternList ] )

RecordComponentPatternList :
Pattern { , Pattern }

记录模式

一个 记录模式 包括一个类型,以及一个(可能为空的)记录组件模式列表,该列表用于与相应的记录组件进行匹配。

例如,给定声明

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) 的简写形式。

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

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

如果一个记录类是泛型的,那么它可以在记录模式中作为参数化类型或原始类型使用。例如:

record Box<T>(T t) {}

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

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

static void test2(Box<String> bo) {
if (bo instanceof Box(var s)) { // Inferred to be Box<String>(var s)
System.out.println("String " + s);
}
}

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

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

在此处,嵌套模式 Box(var s) 的类型参数是推断出来的。如果同样省略外部记录模式中的类型参数,代码会更加简洁:

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 的 case 标签匹配 Bar 类型及其所有可能的子类型的值。

涉及记录模式的模式标签而言,分析起来更为复杂,因为我们必须考虑组件模式的类型,并为 sealed 层次结构做出调整。例如,考虑以下声明:

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 的情况:

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

这两个开关是详尽无遗的,因为接口 Isealed(密封)的,因此类型 CD 涵盖了所有可能的实例:

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 的配对相匹配的情况:

switch (p2) {                        // Error!
case Pair<I>(C fst, D snd) -> ...
case Pair<I>(D fst, C snd) -> ...
case Pair<I>(I fst, C snd) -> ...
}

记录模式与增强的 for 语句

如果 R 是一个记录模式,那么形式为的增强型 for 语句

for (R : e) S

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

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

此翻译有以下结果:

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

  • 记录模式 R 必须详尽无遗地覆盖数组或 Iterable 的元素类型。

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

例如:

record Pair(Object fst, Object snd){}

static void notApplicable(String[] arg) {
for (Pair(var fst, var snd): arg) { // Compile-time error, pattern not applicable
System.out.println("An element");
}
}

static void notExhaustive(Pair[] arg) {
for (Pair(String s, String t): arg) { // Compile-time error, pattern not exhaustive
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) { // Run-time MatchException
System.out.println(f);
}
}

未来工作

这里描述的记录模式可以扩展的方向有很多:

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

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

依赖

本 JEP 基于 JEP 394instanceof 的模式匹配),该功能已在 JDK 16 中提供。