解构数据对象的老痛点
做对账时我拿到一个嵌套的支付结果对象,传统写法要一层层 getter 把字段掏出来,还得判空,代码又臭又长。更糟的是改一个字段层级,所有取值点都得跟着改,漏一处就是运行时 NullPointerException,而且这种错编译期发现不了,只能等线上炸。JDK 21 的模式匹配把 record 和 instanceof 结合起来,能在一行里完成"判断类型加解构字段",我重构了一遍,清爽很多,也顺手把几处隐藏的空指针风险消了。
基础 record pattern
先定义两个 record,然后用 instanceof 模式直接解构,不用先 cast 再 get:
record Point(int x, int y) {}
record Line(Point start, Point end) {}
Object obj = new Line(new Point(0,0), new Point(3,4));
if (obj instanceof Line(Point start, Point end)) {
System.out.println(start.x() + "," + end.x());
}
instanceof 后面直接把 Line 的组件解构到 start、end,再对 Point 继续解构。不用写一堆 getter 和强制转换,判空也省了,因为模式本身要求类型匹配才会进分支。对比老写法要先 cast 再 get,这里一步到位,而且类型不对根本进不了分支,比 instanceof 之后再强转安全。
嵌套解构
JDK 21 支持在模式里直接嵌套,省掉中间变量,这在对账这种深层级场景特别有用:
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
double len = Math.hypot(x2 - x1, y2 - y1);
System.out.println("线段长度 " + len);
}
原来要先取 Line 再取两个 Point 再取坐标,四行变一行。我们的对账代码字段更深(支付结果里嵌渠道、再嵌金额、再嵌币种),嵌套解构把 30 行缩到 9 行,可读性反而更高,因为数据结构和处理逻辑贴在一起,不会在取值过程中走丢。之前那段代码最怕的就是中间某层是 null,现在解构模式天然要求非 null,从源头消灭了 NPE。
和 sealed 一起用,类型安全拉满
配合 sealed 接口,编译器知道所有可能子类型,switch 模式匹配可以穷举。我们拿它做了个表达式求值,原本用 if-else 判断类型,漏过一种就线上算错:
sealed interface Expr permits Num, Add, Mul {}
record Num(int v) implements Expr {}
record Add(Expr l, Expr r) implements Expr {}
record Mul(Expr l, Expr r) implements Expr {}
int eval(Expr e) {
return switch (e) {
case Num(int v) -> v;
case Add(Expr l, Expr r) -> eval(l) + eval(r);
case Mul(Expr l, Expr r) -> eval(l) * eval(r);
};
}
这里有个关键点:如果哪天加了个 Div 子类型却忘了在 switch 里处理,编译器直接报错"switch 表达式不穷尽",把隐患挡在编译期。这是 record 加 sealed 加模式匹配组合最香的地方——用类型系统代替运行时分支遗漏。我们之前用 if-else 判断类型,漏过一个子类型导致线上少算一种优惠,改用 sealed 之后这种错在编译期就暴露了,不会再流到生产。
一个容易忽略的限制
record pattern 解构的是 record 的组件,普通 class 不能直接这么解构,得自己写解构方法或保持 getter。另外解构时变量名作用域只在对应分支内,别指望在分支外访问。还有,record 是浅不可变的,嵌套里的 Point 引用如果外面被改,解构出的仍是同一个引用——需要深拷贝时得自己处理。最后提醒,record 的 equals 是基于全部组件的,用 record 当 Map 的 key 要确认组件不会被改。
写在后面
现在回头看,《Record 模式与模式匹配的完整形态》本身不算多难,难的是线上真出问题那十分钟里的判断。经验都是这么来的。