一、摘要
使用记录模式(Record Patterns)来封装值,从而增强 Java 编程语言的功能。记录模式和类型模式(Type Patterns)可以嵌套,以实现强大、声明性和可组合的数据导航和处理形式。
二、历史
记录模式由 JEP 405 作为预览功能提出,并在 JDK 19 中发布;JEP 432 进行了第二次预览,并在 JDK 20 中发布。该功能与用于切换的模式匹配(Pattern Matching)(JEP 441)共同发展,两者之间存在大量交互。根据不断积累的经验和反馈意见在本次发布时进一步完善该功能。
三、设计目标
将模式匹配(pattern matching)扩展到重构记录类实例,从而实现更复杂的数据查询。
增加嵌套模式,实现更多可组合的数据查询。
四、动机
在 Java 16 中,JEP 394 扩展了 instanceof 操作符,使其可以使用类型模式并执行模式匹配。这一适度的扩展简化了我们熟悉的 instanceof-and-cast 惯用方法,使其更加简洁,更不容易出错:
// Java 16 之前
if (obj instanceof String) {
String s = (String)obj;
... 强制转换后使用 s 变量 ...
}
// 截止 Java 16
if (obj instanceof String s) {
... 直接使用 s 变量,不用再强制转换 ...
}
在新代码中,如果运行时 obj 的值是 String 的实例,则 obj 与类型模式 String s 匹配。如果模式匹配,那么 instanceof 表达式为真,模式变量 s 将初始化为 obj 的字符串值,然后可以在包含的代码块中使用。
类型模式一举消除了许多转换现象。然而,这只是向更声明化、更注重数据的编程风格迈出的第一步。由于 Java 支持新的、更具表现力的数据建模方式,模式匹配可以使开发人员表达其模型的语义意图,从而简化此类数据的使用。
4.1.模式匹配和记录
记录(JEP 395)是透明的数据载体。接收记录类实例的代码通常会使用内置的方法来提取数据。例如,我们可以使用类型模式来测试一个对象是否是记录类 Point 的实例,如果是,则从该对象中提取 x 和 y 的值:
record Point(int x, int y) {}
// 截止 Java 16 的写法
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 组件,并默认调用取值的方法,那就更方便了:
// 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) 是一个记录模式。它将记录执行模式匹配时通过调用记录模式提供的默认方法访问记录内部的 x,y 并赋值出来。
4.3.嵌套记录模式
模式匹配的真正优势在于,它可以优雅地扩展到匹配更复杂的对象图。例如:
// 截至 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());
}
}
其中,ColoredPoint 值 ul 本身就是一个记录值,我们希望对其进行进一步分解。而记录模式支持嵌套,允许包括嵌套内容的记录对象进一步分解。我们可以在记录模式中嵌套另一个模式,并同时分解多层次的对象属性:
// 从 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 s 和 String t。如果一个值是一对字符串,并且其组件值符合类型模式 String s 和 String t,那么该值就符合模式 Pair(String s, String t),上面的例子中,值是一对 int 类型,所以不符合字符串的 Pair 记录模式,不能匹配。
总之,嵌套模式避免了获取对象成员属性值的复杂性,因此我们可以专注于这些对象所表达的数据。嵌套模式还能让我们集中处理错误,因为如果一个值与嵌套模式 P(Q) 中的一个或两个子模式都不匹配,那么该值就与整个嵌套模式 P(Q) 不匹配。我们不需要检查和处理每个子模式的匹配失败——要么整个模式匹配,要么不匹配。
五、说明
我们用可嵌套记录模式扩展了 Java 编程语言。
模式的语法变成:
Pattern:
TypePattern
RecordPattern
TypePattern:
LocalVariableDeclaration
RecordPattern:
ReferenceType ( [ PatternList ] )
PatternList :
Pattern { , Pattern }
5.1.记录模式
记录模式由记录类类型和模式列表(可能为空)组成,模式列表用于匹配相应记录的属性值。
例如,声明一个记录:
record Point(int i, int j) {}
如果值 v 是记录类型 Point 的实例,则该值与记录模式 Point(int i, int j) 匹配;如果匹配,模式变量 i 将被初始化为在值 v 上调用与 i 对应的访问器方法的结果,模式变量 j 将被初始化为在值 v 上调用与 j 对应的访问器方法的结果。与直接调用记录模式 Point(int x, int y) 的作用相同,也是记录模式中成员属性 x 和 y 被初始化为存储的值。
另外需要注意,空值不匹配任何记录模式。
记录模式可以使用 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) ->
... // 推断记录模式 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(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>(Box(var s))
5.2.记录模式和 switch
JEP 441增强了 switch 表达式和 switch 语句,以支持模式标签。switch 表达式和 switch 语句都详尽无遗处理所有可能组合的记录模式:switch 代码块必须有处理记录模式所有可能值的子句。对于模式标签,这是通过对模式类型的分析确定的;例如,case label case Bar b 匹配 Bar 类型的所有可能子类型的值。
对于涉及记录模式的模式标签,分析更加复杂,因为我们必须考虑组件模式(记录模式嵌入其他模式)的类型,并考虑到密封的层次结构。例如,考虑声明:
class A {}
class B extends A {}
sealed interface I permits C, D {} // Java 17 正式发布的特性,密封类
final class C implements I {}
final class D implements I {}
record Pair<T>(T x, T y) {}
Pair<A> p1;
Pair<I> p2;
下面的 switch 检查并非详尽无遗,在定义中,B 继承了 A,所以下面的 switch 检查缺少包含两个类型都为 A 的值的配对检查:
// 从 Java 21 开始
switch (p1) { // Error!
case Pair<A>(A a, B b) -> ...
case Pair<A>(B b, A a) -> ...
}
这两个 switch 检查是详尽的,因为接口I是密封的,因此C型和D类型涵盖了所有可能的实例,然后 I 包含了 C、D 两种类型,所以以下两个 switch 检查穷尽了所有可能:
// 从 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 patterns),用于可变迭代的记录;
- 未命名模式(Unnamed patterns),可出现在记录模式列表中,可匹配任何值,但不声明模式变量;
- 以及可适用于任意类别而非仅记录类别的值的模式。
这些都看未来 JEP 的世纪发展情况吧
七、依赖关系
本 JEP 基于 JDK 16 中提供的针对 instanceof 的模式匹配(JEP 394)。它与针对 switch 的模式匹配(JEP 441)共同发展。