跳到主要内容

JEP 440: 记录模式

QWen Max 中英对照 JEP 440: Record Patterns

总结

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

历史

记录模式被 JEP 405 提议为预览功能,并在 JDK 19 中交付,随后通过 JEP 432 第二次预览并在 JDK 20 中发布。该功能与 switch 的模式匹配 (JEP 441) 共同演进,并与其有显著的交互关系。本 JEP 提议在持续的经验和反馈基础上,通过进一步改进来最终确定该功能。

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

目标

  • 扩展模式匹配以解构 record 类的实例,从而实现更复杂的数据查询。

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

动机

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

// Prior to Java 16
if (obj instanceof String) {
String s = (String)obj;
... use s ...
}

// As of Java 16
if (obj instanceof String s) {
... use s ...
}

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

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

模式匹配与记录

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

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

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

嵌套记录模式

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

// As of 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) {}

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

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

但是 ColoredPoint 类型的值 ul 本身是一个记录值,我们可能希望进一步将其分解。因此,记录模式支持嵌套,这允许记录的组成部分通过嵌套模式进一步匹配和分解。我们可以在记录模式中嵌套另一个模式,从而一次性分解外部记录和内部记录:

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

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

Rectangle(Point(0, 0), Point(10, 20))  
// As of Java 16
Rectangle r = new Rectangle(new ColoredPoint(new Point(x1, y1), c1),
new ColoredPoint(new Point(x2, y2), c2));

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

// As of 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);
}
}

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

// As of 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 }

记录模式

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

例如,给定声明

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

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

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

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

// As of 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) ->
... // Inferred record pattern MyPair<String,Integer>(var f, var s)
...
}
}

所有支持记录模式的结构都支持推断类型参数,即 instanceof 表达式以及 switch 语句和表达式。

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

// As of 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)

事实上,也可以在外层记录模式中省略类型参数,从而得到简洁的代码:

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

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

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

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

// As of Java 21
switch (p2) { // Error!
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)共同演进。