跳转到内容

JEP 440:记录模式

原文:https://openjdk.org/jeps/440
翻译:张欢

使用记录模式来解构记录值,从而增强 Java 编程语言。记录模式和类型模式可以嵌套,从而实现有力的、声明式的、可组合的数据导航和处理形式。

历史

记录模式由JEP 405提出作为预览功能,并在JDK 19中交付。然后并由JEP 432进行第二次预览,并在JDK 20中交付。此功能与switch的模式匹配JEP 441)共同发展,两者有相当大的交互。本JEP提议根据持续的经验和反馈进一步完善此功能。

除了一些细微的编辑性更改外,自第二次预览以来的主要变化是,删除了对出现在增强for语句头部中记录模式的支持。该功能可能会在未来的JEP中重新提出。

目标

  • 扩展模式匹配以解构record类的实例,从而实现更复杂的数据查询。
  • 添加嵌套模式,从而实现更多可组合的数据查询。

动机

在Java 16中,JEP 394扩展了instanceof运算符,使其能够采用类型模式并执行模式匹配。这种适度的扩展简化了熟悉的“instanceof-and-cast”用法,使其更加简洁且不易出错:

// Java 16之前
if (obj instanceof String) {
String s = (String)obj;
... use s ...
}
// 从Java 16开始
if (obj instanceof String s) {
... use s ...
}

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

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

模式匹配与记录

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

// 从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 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 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 21开始
static void printUpperLeftColoredPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
System.out.println(ul.c());
}
}

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

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

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

// 从Java 16开始
Rectangle r = new Rectangle(new ColoredPoint(new Point(x1, y1), c1),
new ColoredPoint(new Point(x2, y2), c2));

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

// 从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 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,则该值与模式Pair(String s, String t)进行匹配,并且其组件值以递归方式与类型模式String sString t进行匹配。在上面的示例代码中,这些递归模式匹配会失败,因为两个记录组件值都不是字符串,因此执行else块。

总之,嵌套模式消除了导航对象的意外复杂性,这样我们就可以专注于这些对象所表达的数据。它们还使我们能够集中处理错误,因为如果其中一个或两个子模式不匹配,则取值将无法匹配嵌套模式P(Q)。我们不需要检查和处理每个单独的子模式匹配失败——要么整个模式匹配,要么整个模式不匹配。

描述

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

模式的语法变为:

模式:
类型模式
记录模式
类型模式:
局部变量声明
记录模式:
引用类型 ( [ 模式列表 ] )
模式列表:
模式 { , 模式 }

记录模式

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

例如,给定声明:

record Point(int i, int j) {}

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

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

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

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

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

如果记录模式命名为泛型记录类,但没有提供类型参数(即记录模式使用原始类型),则始终会推断类型参数。例如:

// 从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语句和表达式。

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

// 从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 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的所有可能子类型。

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

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

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

// 从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) { // 错误!
case Pair<I>(C fst, D snd) -> ...
case Pair<I>(D fst, C snd) -> ...
case Pair<I>(I fst, C snd) -> ...
}

未来的工作

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

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

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

依赖

本JEP以JDK 16中提供的instanceof的模式匹配JEP 394)为基础。它与switch的模式匹配JEP 441)共同发展。