跳到主要内容

JEP 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 习惯用法,使其更加简洁且不易出错:

// 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 s``obj``String``instanceof``true``s``obj``String

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

模式匹配和记录

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

// 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()的值。 (在每个记录类中,其访问器方法与其组件之间都存在一一对应的关系。)如果该模式不仅能够测试某个值是否是 的实例,而且还能够从该值中提取和组件,那就更好了直接代表我们调用访问器方法。换句话说:x``y``Point``x``y

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

嵌套记录模式

模式匹配的真正力量在于它可以优雅地扩展以匹配更复杂的对象图。例如,考虑以下声明:

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

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

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

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

// 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 tPair(String s, String t)如果值是 a ,则它与模式匹配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它与记录模式匹配;如果是,则用调用value 上对应的访问器方法的结果来初始化模式变量,并将模式变量初始化为调用value 上对应的访问器方法的结果。 (模式变量的名称不需要与记录组件的名称相同;即,除了模式变量和被初始化之外,记录模式的行为相同。)Point(int i, int j)``Point``i``i``v``j``j``v``Point(int x, int y)``x``y

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 标签case Bar b匹配 type 的值Bar以及 的所有可能的子类型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) -> ...
}

这两个开关是详尽的,因为接口和类型都I涵盖了所有可能的实例:sealed``C``D

// 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) -> ...
}

未来的工作

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

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

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

依赖关系

该 JEP 基于 JDK 16 中提供的模式匹配instanceof( JEP 394 )构建。它与_模式匹配switch_( JEP 441 )共同发展。