Java record 实战:不可变数据载体、紧凑构造器校验与什么时候别用
Java record 实战:不可变数据载体、紧凑构造器校验与什么时候别用写 Java 的都干过这件苦力活:一个只用来装数据的类,要手写字段、构造器、getter、equals、hashCode、toString,几十行全是模板代码。publicclassPoint{privatefinalintx;privatefinalinty;publicPoint(intx,inty){this.xx;this.yy;}publicintgetX(){returnx;}publicintgetY(){returny;}Overridepublicbooleanequals(Objecto){if(thiso)returntrue;if(!(oinstanceofPoint))returnfalse;Pointp(Point)o;returnxp.xyp.y;}OverridepublicinthashCode(){returnObjects.hash(x,y);}OverridepublicStringtoString(){returnPoint{xx, yy};}}30 行代码,真正有信息量的就是「一个 Point 有 x 和 y」。剩下全是编译器完全可以帮你写的东西。Java 16 正式落地的record就是干这个的。这篇讲怎么用它、以及什么时候不该用它。一行顶三十行上面那个Point用 record 写就是一行:publicrecordPoint(intx,inty){}这一行帮你自动生成了:两个private final字段x、y;一个全参构造器Point(int x, int y);两个访问器x()和y()(注意:不是getX(),是x(),方法名就是字段名);基于全部字段的equals()和hashCode();一个可读的toString(),形如Point[x1, y2]。直接用:Pointp1newPoint(1,2);Pointp2newPoint(1,2);System.out.println(p1.x());// 1 —— 访问器是 x() 不是 getX()System.out.println(p1);// Point[x1, y2]System.out.println(p1.equals(p2));// true —— 值相等,不用自己写 equalsrecord 里的字段叫「组件(components)」,括号里声明的组件顺序、名字直接决定了构造器参数和访问器名字。校验参数:紧凑构造器数据类经常需要在构造时校验,比如金额不能为负、名字不能为空。record 提供了「紧凑构造器(compact constructor)」,不用写参数列表,直接写校验逻辑:publicrecordMoney(Stringcurrency,longcents){// 紧凑构造器:没有参数列表,方法名就是 record 名publicMoney{if(cents0){thrownewIllegalArgumentException(金额不能为负: cents);}if(currencynull||currency.isBlank()){thrownewIllegalArgumentException(币种不能为空);}// 还能对参数做规范化,赋值会自动写回同名字段currencycurrency.toUpperCase();}}注意两个关键点:没有Money(String currency, long cents)参数列表,直接public Money {。编译器会在这段逻辑执行完后,自动把参数值赋给对应字段。在紧凑构造器里给currency重新赋值,会写回字段。所以currency currency.toUpperCase()能做规范化,不需要写this.currency ...。MoneymnewMoney(usd,500);System.out.println(m.currency());// USD —— 被规范化成大写newMoney(USD,-1);// 抛 IllegalArgumentException这是新手常踩的坑:以为紧凑构造器里还要手动this.x x。不用——你只管校验和规范化参数,赋值编译器包了。record 可以有额外方法和静态工厂record 不是只能装数据,它可以有普通方法、静态方法、静态字段、实现接口:publicrecordRange(intstart,intend){publicRange{if(startend)thrownewIllegalArgumentException(start 不能大于 end);}// 普通实例方法publicintlength(){returnend-start;}publicbooleancontains(intv){returnvstartvend;}// 静态工厂方法,给调用方更语义化的入口publicstaticRangeof(intstart,intend){returnnewRange(start,end);}}RangerRange.of(3,10);System.out.println(r.length());// 7System.out.println(r.contains(5));// truerecord 还能实现接口,常用来配合 sealed 接口做「代数数据类型」:sealedinterfaceShapepermitsCircle,Rectangle{}recordCircle(doubleradius)implementsShape{}recordRectangle(doublew,doubleh)implementsShape{}// 配合 switch 模式匹配(Java 21 稳定),编译器能检查是否覆盖所有子类型staticdoublearea(Shapes){returnswitch(s){caseCirclec-Math.PI*c.radius()*c.radius();caseRectangler-r.w()*r.h();};}一个真实场景:DTO 与 API 返回体record 最实用的场景就是 Spring 里的 DTO / 请求体 / 响应体。以前一个响应类要写一大坨,现在:publicrecordUserResp(Longid,Stringname,Stringemail){}GetMapping(/users/{id})publicUserRespgetUser(PathVariableLongid){UseruuserService.findById(id);returnnewUserResp(u.getId(),u.getName(),u.getEmail());}Jackson 从 2.12 起原生支持 record 序列化/反序列化,上面这个直接就能当 JSON 返回,也能当RequestBody接收。请求参数校验也能配合紧凑构造器或 Bean Validation 注解一起用。什么时候别用 record —— 三条硬边界record 好用,但它有明确的适用边界,用错了会很别扭:1. record 是不可变的,字段全是final。需要「创建后还能改字段」的可变对象(比如 JPA 实体、需要 setter 的表单对象),不要用 record。PointpnewPoint(1,2);// p.x 5; 编译错误:x 是 final,record 没有 setter2. 它不能继承别的类(record 隐式继承java.lang.Record,Java 单继承)。需要extends某个基类的场景用不了 record。但它可以实现接口,所以「继承」需求多数能用接口 组合绕过。3. equals/hashCode 基于全部字段。如果你的相等语义只看主键(比如实体只按 id 判等),record 的「全字段判等」就不对——这也是不要拿 record 当 JPA 实体的原因之一:JPA 实体需要可变、需要无参构造器、相等语义通常只看 id,三条全和 record 冲突。一句话判断:这个类是不是「一组值的只读快照」?是,就用 record(DTO、配置、事件、坐标、返回体);不是(有生命周期、要改状态、要按 id 判等的领域实体),就老老实实用普通类。小结record 一行生成字段、构造器、访问器、equals/hashCode/toString,专治数据类模板代码。访问器是x()不是getX();组件声明顺序决定构造器参数。校验/规范化用紧凑构造器:不写参数列表、不写this.xx,赋值编译器代劳。可以有额外方法、静态工厂、实现接口,配合 sealed switch 模式匹配很强。别用 record 的三种情况:需要可变字段、需要继承类、相等语义只看主键(典型如 JPA 实体)。记忆点:record 是「不可变值对象」的语法糖,判断标准就一句——它是不是一组值的只读快照。