instanceof 还能拆字段?Java 21 记录模式把对象一行拆平
系列:Java 后端实战精通营 · 作者:鱼宵
配套课程仓库:https://gitee.com/j67mk2/java21-journey (第 6 课)
先看一段"两层嵌套就看不下去"的代码
假设有个几何系统:圆(Circle)里套着圆心(Point),矩形(Rectangle)左上角也是个点。你要按形状打印描述,老写法是这样的:
if(sinstanceofCircle){Circlec=(Circle)s;// 第一层强转Pointp=c.center();// 取圆心doublex=p.x();// 再取坐标doubley=p.y();// 第三层了……}两层嵌套就要两层强转 + 链式访问。等业务变成"订单套地址、地址套城市",这段代码谁看谁头大——强转链越长,越容易转错类型。
Java 21 的记录模式(Record Pattern)就是干这个的:类型判断、自动转型、拆字段三合一,一行拆到最内层。
一句话结论:记录模式 = 匹配类型的同时把 record 组件"拆"出来
记录模式(Record Pattern,JEP 440,JDK 21 正式特性):在instanceof/case后面写类型(组件类型 变量, ...),匹配成功就自动转型 + 把 record 的各个组件绑定给新变量。
类比:快递站拆包裹。老写法是先验货(instanceof 判断)、签收盖章(强转)、再一件件掏出来点数(手动调
x()、y());记录模式是快递员一边验货一边把东西摊在你桌上——Point(int x, int y)就是"如果是 Point 包裹,把里面 x、y 两件货直接递给我"。
关键代码:老写法 vs 记录模式(完整可运行)
先看"检查依据"——密封图形家族(第 4 课 sealed + 第 5 课 switch 的知识点,本课合体):
/** 密封家族:Circle、Rectangle 两个成员,switch 才敢不写 default */publicsealedinterfaceShapepermitsCircle,Rectangle{}recordPoint(intx,inty){}// 点recordCircle(Pointcenter,doubler)implementsShape{}// 圆:嵌套了 PointrecordRectangle(PointtopLeft,doublew,doubleh)implementsShape{}// 矩形:也嵌套了 Point① instanceof 记录模式——三行变一行:
// 老写法:判断 + 强转 + 取字段,三步if(pointObjinstanceofPointp){intx=p.x();inty=p.y();// ... 用 x、y}// 新写法:一个动作全做完if(pointObjinstanceofPoint(intx,inty)){// 直接就能用 x、y}instanceof Point(int x, int y):一个动作完成三件事——判断是不是 Point、是就自动转型、再把两个组件分别绑给变量 x、y。- 不匹配时整个条件为 false,x、y 不会被绑定,后面的分支里根本不能用它们——编译器帮你保证安全,不会出现"用了没绑定的变量"。
② switch 嵌套解构——一行拆到最内层:
staticStringdescribe(Shapes){returnswitch(s){caseCircle(Point(intx,inty),doubler)->"圆心("+x+", "+y+"),半径 "+r;caseRectangle(Point(inttx,intty),doublew,doubleh)->"左上角("+tx+", "+ty+"),宽 "+w+",高 "+h;};}Circle(Point(int x, int y), double r):嵌套解构。外层匹配 Circle;第一个组件是 Point,括号里继续拆出 x、y;第二个组件直接绑给 r。一行拆到最内层——老写法要((Circle) s).center().x()这种强转 + 链式访问,两层嵌套就要两层强转。- Shape 是 sealed 且两个分支全覆盖,不需要 default(第 5 课同款穷尽性检查)。
- 顺便:拆出来的组件本身是个 record 就接着拆,这就是俄罗斯套娃——“开大圆、再开圆心那个小点”。
③ 算面积不想拆,用 var 接住:
staticdoublearea(Shapes){returnswitch(s){caseCircle(varcenter,doubler)->Math.PI*r*r;// 不关心圆心坐标,var 整个接住caseRectangle(vartopLeft,doublew,doubleh)->w*h;};}var center:类型照判(是 Circle 就行),但不拆组件。拆不拆随你——想拆写Point(int x, int y),不想拆写var。嵌套层深(订单套地址套城市),不想拆的那层就var接住。
④ 泛型坑:Box<String>写不了,Box<?>可以:
recordBox<T>(Tcontent){}// 报错!Object cannot be safely cast to Box<String>// if (box instanceof Box<String>(String s)) { ... }// 正确写法:通配符让泛型"退一步"if(boxinstanceofBox<?>(Objectcontent)){System.out.println("箱子里装着:"+content);}Box<String>不是可具体化类型(reifiable):泛型类型参数编译后就被**擦除(erasure)**了,运行时Box<String>和Box<Integer>是同一个类Box,JVM 根本没机会验证"里面装的是不是 String",编译器宁可报错也不让你写这种"看似安全实则无法验证"的代码。Box<?>在运行时就是"一个 Box,内容不知道"——这个判断是真的能做的,所以能解构。
实测输出:新旧写法行为一致,嵌套解构生效
我本机(JDK 21.0.12.1)的真实输出:
========== 第 6 课:记录模式 ========== --- 1) instanceof 老写法 vs 记录模式新写法 --- 老写法拿到:point(3, 4) 新写法拿到:point(3, 4) textObj 不是 Point,解构没发生:我不是 Point --- 2) switch 嵌套解构:一行拆到最内层 --- 描述圆:圆心(0, 1),半径 2.5 描述矩形:左上角(2, 3),宽 4.0,高 5.0 圆面积:19.634954084936208 矩形面积:20.0 --- 3) 记录模式与泛型:Box<?> 可以,Box<String> 不行 --- 箱子里装着:一封信输出怎么读:
老写法拿到和新写法拿到输出完全一样——记录模式只是少写代码,行为没变,重构安全。textObj 不是 Point那行:传进去的是字符串,解构条件为 false 走 else——不匹配就不解构。描述圆:圆心(0, 1),半径 2.5:嵌套解构Circle(Point(int x, int y), double r)生效,x=0、y=1 是从 Point 里拆出来的。矩形面积:20.0:4 × 5 = 20,var topLeft接住整个点、不解构,面积照常算。箱子里装着:一封信:Box<?>(Object content)解构成功。
挑战题(答案都在课程源码里)
- ⭐照猫画虎:给 Shape 家族加
record Line(Point start, Point end) implements Shape(记得更新 permits),在 describe 里加一个嵌套解构分支。 - ⭐改守卫:给 describe 加一条 when——“半径 > 10 的圆叫大圆”,看守卫子句怎么和嵌套解构配合。
- ⭐⭐试报错:把
Box<?>(Object content)改成Box<String>(String s)亲手编译一次,把报错原文记下来(Object cannot be safely cast to Box<String>)。 - ⭐⭐var 对照:把 area 里的
var center改成Point center再编译——体会"写全类型 = 要拆,var = 不拆"的差别。 - ⭐⭐⭐挑战:给 Point 加第三个组件
int z(三维点),不修改 describe 直接编译——观察嵌套解构的编译报错,体会"record 加字段,解构代码被编译器逼着同步改"。
面试回答模板
问:记录模式了解吗?举个例子。
- 一句话结论:记录模式是 JDK 21 转正的特性,让 instanceof 和 switch 在类型匹配的同时把 record 的组件直接拆出来绑给变量。
- 展开 1~2 句原理/例子:比如
case Circle(Point(int x, int y), double r)一行就能拆到圆心坐标,老写法要强转再链式访问;sealed 家族下还能做穷尽性检查,不用写 default。 - 收一句应用场景:实际项目里处理"树形结构的数据"很常见,比如订单里嵌地址、报文里嵌对象,用记录模式比层层强转干净得多,也不容易漏分支。
预判追问:“Box 为什么不行?”——“泛型实参编译后擦除,运行时无法验证类型,报Object cannot be safely cast,变通写Box<?>”;追问"解构会改对象吗?"——“不会,record 不可变,解构只读,绑出来的是快照”。
总结表:坑 → 原因 → 解法
| 坑 | 原因 | 解法 |
|---|---|---|
| 嵌套对象强转链看不下去 | 判断 + 强转 + 取字段绞在一起 | 记录模式嵌套解构一行拆平 |
Box<String>编译报错 | 泛型擦除,运行时无法验证 | Box<?>+Object content |
| 解构括号和组件数对不上 | record 组件变了解构没同步 | 编译器直接报错,逼你同步改 |
| 以为解构能改对象 | 误把解构当修改 | record 不可变,解构只读 |
| switch 不写 default 报"不完整" | 输入不是 sealed 家族 | sealed 圈死家族,或补 default |
describe(null)抛 NPE | switch 对 null 求值 | 显式加case null -> |
关于这个系列
本文属于Java 后端实战精通营系列(Java 21 实战 10 课)。完整代码在java21-journey 课程第 6 课(lesson-06/src/main/java/com/java21/lesson06/,含Shape.java图形家族 +Main.java三组演示):
gitclone https://gitee.com/j67mk2/java21-journey.gitcdjava21-journey/lesson-06 mvn-qcleaninstall-DskipTestsjava-cptarget/classes com.java21.lesson06.Main(仓库地址是小写 j 的 j67mk2,复制别丢字母。跑完会看到新旧写法输出一致、嵌套解构拆到圆心、Box<?> 解构成功。)
下一篇预告:顺序集合——SequencedCollection让"拿最后一个元素"从list.get(size-1)变成getLast(),增删首尾元素也不再是土办法。
掏心窝一句:记录模式 + sealed + switch 是三兄弟,前两课学的在这里"合体"。面试官问"Java 21 新特性",你把这三件套串成一个故事(sealed 圈家族 → switch 按类型分派 → 记录模式拆字段),比零散背特性名高一个段位。跑出输出和我不一样?评论区一起查。
【发布信息】
- 主标题:instanceof 还能拆字段?Java 21 记录模式把对象一行拆平
- 备选标题 2:订单套地址、圆里套点:Java 21 记录模式嵌套解构一行拆到最内层
- 备选标题 3:Java 21 记录模式:类型判断+转型+拆字段三合一,Box 却写不了
- 标签建议:java / Java21 / record
- 摘要建议:圆里套点、订单套地址,嵌套对象的老写法要强转 + 链式访问,两层就看不下去了。Java 21 记录模式让 instanceof 和 switch 在匹配类型的同时把 record 组件直接拆出来——一行拆到最内层,顺带讲清 Box 为什么写不了(泛型擦除)。附完整代码、实测输出与面试回答模板。