写个订单类要 55 行?Java 21 的 record 一行搞定

发布时间:2026/10/30 2:47:14
写个订单类要 55 行?Java 21 的 record 一行搞定

写个订单类要 55 行?Java 21 的 record 一行搞定

系列:Java 后端实战精通营 · 作者:鱼宵
配套课程仓库:https://gitee.com/j67mk2/java21-journey (第 4 课)

先讲一个"加个字段改出 bug"的真实现场

你肯定见过这种代码:一个订单类,字段七八个,构造器、getter、equals、hashCode、toString 手写五六十行。然后某天需求来了——“订单加一个优惠券字段”。

手快的同学在字段区加了一行,getter 加了一行,忘了改 equals。于是:两笔内容完全不同的订单(一个用了优惠券、一个没用),equals返回true——去重逻辑直接失效,同一个订单被当成两个,库存扣了两遍。

这个 bug 的根源不是粗心,是样板代码全靠手写,字段一变就失同步。Java 21 的record就是冲着这个来的。

一句话结论:record = 纯数据类语法糖,样板代码全自动

Record(记录类):你只声明"有哪几个字段",构造器、equals、hashCode、toString、访问器由编译器自动生成,字段隐式final不可变。

类比:智能表格。以前写订单类像一笔一笔手抄账(字段、构造器、getter、equals、toString 六七十行);record 只写"有哪几列",剩下的账房先生(编译器)自动算好,你想抄错都没机会。

关键代码:55 行 POJO vs 一行 record(完整可运行)

先看"麻烦长什么样"——传统手写 POJO 的核心部分:

publicfinalclassLegacyOrder{privatefinalStringorderId;privatefinalStringitem;privatefinaldoubleamount;// 手写构造器publicLegacyOrder(StringorderId,Stringitem,doubleamount){this.orderId=orderId;this.item=item;this.amount=amount;}// 手写 getter ×3:getOrderId() / getItem() / getAmount()publicStringgetOrderId(){returnorderId;}publicStringgetItem(){returnitem;}publicdoublegetAmount(){returnamount;}// 手写 equals:每个字段都比一遍,字段多了必漏@Overridepublicbooleanequals(Objecto){if(this==o)returntrue;if(!(oinstanceofLegacyOrderthat))returnfalse;returnDouble.compare(that.amount,amount)==0&&orderId.equals(that.orderId)&&item.equals(that.item);}// 手写 hashCode + toString(各十几行)…… 全加起来约 55 行}

再看 record——同样的事,一行声明:

/** * record:一行声明三个字段,构造器/equals/hashCode/toString/访问器全部自动生成 * 字段默认 final(不可变),类默认 final(不能继承) */publicrecordModernOrder(StringorderId,Stringitem,doubleamount){/** 紧凑构造器:不写参数列表,只写校验/清洗,赋值编译器自动补 */publicModernOrder{if(amount<0){thrownewIllegalArgumentException("金额不能为负:"+amount);}if(item==null||item.isBlank()){thrownewIllegalArgumentException("商品名不能为空");}orderId=orderId==null?null:orderId.trim();// 清洗:去空格}}

这段代码讲什么,三句话:

  • record ModernOrder(String orderId, String item, double amount):一行声明三个字段。编译器自动生成构造器、三个访问器、equals、hashCode、toString——五六十行样板,一行搞定。
  • 紧凑构造器public ModernOrder { ... }:record 专属写法,没有参数列表,只写校验和清洗逻辑。金额为负、商品名为空,在对象"出生"那一刻就被拦下——非法对象根本建不出来。这比"先 new 出来再慢慢校验"安全一个量级。
  • orderId = orderId.trim():在紧凑构造器里给参数重新赋值,编译器会把它当作最终存进字段的值。不写这行,传进来的" A003 "会连空格一起存进去。

访问器注意:record 的访问器是r1.item()(和字段同名的方法),不是getItem()——写getItem()编译直接报错。

顺带把 sealed 密封类也讲了(下一课的地基)

sealed(密封)关键字:“谁能继承/实现我,由我自己说了算”,搭配permits列出允许的名单:

/** sealed 接口:只允许名单上的三个形状实现 */publicsealedinterfaceShapepermitsCircle,Rectangle,Square{doublearea();// 每种形状自己算面积/** 固定骨架:先打形状名、再打面积(模板方法) */defaultStringdescribe(){returnthis.getClass().getSimpleName()+",面积 = "+String.format("%.2f",area());}}recordCircle(doubleradius)implementsShape{publicdoublearea(){returnMath.PI*radius*radius;}}recordRectangle(doublewidth,doubleheight)implementsShape{publicdoublearea(){returnwidth*height;}}recordSquare(doubleside)implementsShape{publicdoublearea(){returnside*side;}}

类比:银行 VIP 室的准入名单。普通接口是"谁都能进";sealed 是门口挂了块牌子:“只认名单上这三家”,名单外的人想实现它?编译直接报错。

为什么这很重要——穷尽性(exhaustiveness):sealed 把子类名单固定下来之后,编译器才敢替你做"漏没漏"检查。名单上就 Circle、Rectangle、Square 三个,你的分派代码把三个都覆盖了,就叫穷尽——这也是下一课模式匹配 switch 能省略default的前提。没有 sealed,别人随时加个新实现类,你的 if-else 链就漏了。

类比:点名册。名单上就三个人,点全了才叫穷尽——少点一个,老师(编译器)当场喊出来。

实测输出:equals 自动比较、非法订单被拦

我本机(JDK 21.0.12.1)的真实输出:

========== 第 4 课:Record 与密封类 ========== --- 1) 传统 POJO 打印(手写的 toString)--- l1 = LegacyOrder{orderId='A001', item='机械键盘', amount=299.0} --- 2) record 打印(编译器自动生成的 toString)--- r1 = ModernOrder[orderId=A001, item=机械键盘, amount=299.0] --- 3) equals 与 hashCode:值相等就该相等 --- r1.equals(r2)(内容相同)= true r1.equals(r3)(内容不同)= false r1.hashCode() = -1835803728,r2.hashCode() = -1835803728,相等对象哈希值相同:true --- 4) 紧凑构造器校验:负数订单应该被拒绝 --- 捕获到异常:金额不能为负:-50.0 捕获到异常:商品名不能为空 传入 orderId=" A003 ",实际存进去的是:[A003] --- 5) sealed Shape:形状家族,子类被 permits 圈定 --- Circle,面积 = 78.54 Rectangle,面积 = 24.00 Square,面积 = 9.00 总面积 = 111.54

输出怎么读:

  • ModernOrder[...]是 record 自动生成的 toString(方括号 + 字段名=值),打印出来一目了然。
  • r1.equals(r2) = true、r1.equals(r3) = false:自动 equals 逐字段比较——回到开头的 bug,record 从根上消灭了"加字段忘了改 equals"这类事故。两行 hashCode 相同:相等对象哈希值也相等,放 HashMap 当 key 不会翻车。
  • 两条"捕获到异常":紧凑构造器把金额为负、商品名为空的订单在创建时就拒了,对象"出生即合法"。
  • [A003]首尾无空格:紧凑构造器里的trim()清洗生效。
  • 三个形状的面积(78.54 / 24.00 / 9.00)各算各的,但describe()骨架对它们一视同仁——这就是模板方法:骨架固定、细节自由。

挑战题(答案都在课程源码里)

  1. ⭐照猫画虎:照ModernOrder写一个record Student(String id, String name, int score),创建两个学生并打印,看 toString 长什么样。
  2. ⭐加校验:在紧凑构造器里加一条——amount大于 100 万也抛异常(大额订单走人工审核),重新编译验证。
  3. ⭐⭐删 equals 实验:把LegacyOrder手写的 equals 删掉再跑 Main,看l1.equals(同值新对象)输出变成什么——想想"没写 equals"在工程里会踩什么坑(提示:HashMap 的 key)。
  4. ⭐⭐加形状:给 Shape 家族加record Triangle(double base, double height) implements Shape(面积 = 底×高÷2),记得更新permits名单,重新编译跑通。
  5. ⭐⭐⭐挑战:写一个describeAll(List<Shape>),内部用 instanceof 链分形状打印不同格式,体会"家族一扩大,if-else 链就要加一个 else-if"的痛——这就是下节课模式匹配 switch 要解决的问题。

面试回答模板

问:record 是什么?解决了什么问题?

  • 一句话结论:record 是 Java 16 正式发布的数据类语法糖,声明一行字段就自动生成构造器、equals、hashCode、toString 和访问器。
  • 展开 1~2 句原理/例子:字段都是 final,对象天然不可变;equals 逐字段自动比较,从根上消灭了"加字段忘了改 equals"这类 bug;还能在紧凑构造器里做参数校验,非法对象根本建不出来。
  • 收一句应用场景:日常写 DTO、订单、坐标这种纯数据载体时我都用 record,代码量从五六十行缩到一行。

预判追问:“record 能继承吗?”——“不能。它隐式 extendsjava.lang.Record,Java 单继承被占了,所以既不能 extends 别人也不能被 extends”。追问"sealed 呢?"——“sealed 配 permits 圈定谁能继承我,被允许的子类必须是 final / sealed / non-sealed 三选一;它让类型家族封闭,是模式匹配 switch 省略 default 的前提”。

总结表:坑 → 原因 → 解法

坑原因解法
加字段忘了改 equals,订单去重失效样板代码手写,字段一变就失同步数据载体用record,equals 自动逐字段比较
record 编译报错JDK 不是 21(record 要 16+,sealed 要 17+)java -version确认 21
r1.getItem()报错record 访问器是item(),不是 getter 风格用r1.item()
负数金额订单建出来了没在创建时校验紧凑构造器里抛异常,对象出生即合法
sealed 的 permits 名单编译报错名单类必须和接口同包,且必须真实实现确认同包 + 名单上每个类都 implements
想给 record 加字段record 字段在声明括号里写死只能改声明;它只适合纯数据载体

关于这个系列

本文属于Java 后端实战精通营系列(Java 21 实战 10 课)。完整代码在java21-journey 课程第 4 课(lesson-04/src/main/java/com/java21/lesson04/,含LegacyOrder.java传统 POJO、ModernOrder.javarecord、Shape.java密封接口 + 三个形状):

gitclone https://gitee.com/j67mk2/java21-journey.gitcdjava21-journey/lesson-04 mvn-qcleaninstall-DskipTestsjava-cptarget/classes com.java21.lesson04.Main

(仓库地址是小写 j 的 j67mk2,复制别丢字母。跑完会看到传统 POJO 和 record 的打印对比、负数订单被拦、三个形状面积。)

下一篇预告:模式匹配 switch——switch 从"等值判断"升级成"类型匹配",配合 sealed 的穷尽性,default都能省掉。

掏心窝一句:record 这个特性,学的时候觉得"就是少写几行",但它是消灭一整类 bug(equals/hashCode 失同步)的语法。面试官问"Java 21 有什么新特性",你答 record + 紧凑构造器 + sealed + 穷尽性这一套,明显比背名字高一个段位。跑出输出和我不一样?评论区一起查。


【发布信息】

  • 主标题:写个订单类要 55 行?Java 21 的 record 一行搞定
  • 备选标题 2:sealed 密封类:Java 21 让"谁能继承我"由你自己说了算
  • 备选标题 3:从 55 行 POJO 到一行 record:Java 21 把数据类的样板代码全删了(含负数金额拦截)
  • 标签建议:java / Java21 / record
  • 摘要建议:手写订单类五六十行样板代码,加字段忘了改 equals 直接出生产事故。Java 21 的 record 一行声明搞定构造器/equals/hashCode/toString,紧凑构造器还能在对象出生时拦住负数金额;搭配 sealed 密封类实现类型封闭与穷尽性。附完整代码、实测输出与面试回答模板。