登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java record compact constructor 的校验位置

来源:17golang原创

时间:2026-11-01 06:24:54 269浏览 收藏

Java record 的 compact constructor(紧凑构造器)适合放“对象一旦创建就必须满足”的参数校验和规范化。构造器体内直接使用组件名,例如 code、copies;不要给 this.code 赋值,也不要通过访问器读取尚未完成初始化的对象来校验。构造器体正常结束后,编译器才把对应参数的最终值写入组件字段。

我更倾向于把这种校验当成数据对象的入口契约:接口、导入任务和内部调用都应该遵守,而库存是否充足、用户有没有权限等依赖外部状态的判断交给服务层。下面以一个虚构的印制任务参数对象说明这种取舍,示例以 Java 21 为语义基线,没有声称在本机执行或获得线上效果。

Java 官方规范地址:https://docs.oracle.com/javase/specs/jls/se21/html/jls-8.html#jls-8.10.4.2

创建入口增多时,校验为什么不能只留在服务层

假设一个印制任务需要任务代码、份数和标签。最初只有一个 HTTP 入口,开发者可能在控制器里检查份数是否大于零。后来增加批量导入与定时任务,如果它们直接创建相同的 record,入口校验就可能分散成三份。这里的维护压力来自创建路径数量,不能简单归结为请求量。

我会先写下两类规则。第一类只看传入数据,例如代码不能为空、份数必须为正、标签不能含空项;第二类必须查别的系统,例如纸张是否有库存、操作员是否有权限。把第一类约束集中到构造器,让第二类留在业务调用处,职责会清楚得多。这是本文示例的设计建议,并不是 record 自动完成的业务能力。

这种改造也有代价:原来可以暂存非法状态的调用,现在会在构造时失败。若导入页面需要一次展示几十条错误,我会先保留独立的原始输入模型,收集错误后才创建有效 record,而不是让领域对象承载“有一半字段还没修好”的状态。

紧凑构造器里检查参数并规范化

紧凑形式省略的是参数列表和字段赋值样板,参数本身仍存在。JLS 8.10.4.2 规定,构造器体里的组件简单名称指向隐式形参;体内不能向组件字段赋值,不能写 return,也不能显式调用另一个构造器。正常结束之后的隐式字段初始化使用的是参数当时的值。因此,规范化要重新赋给参数。

import java.util.Objects;

public record PrintSpec(String code, int copies) {
    public PrintSpec {
        // 必需值先拒绝空引用,再按本对象约定去掉首尾空白。
        code = Objects.requireNonNull(code, "任务代码不能为空").strip();
        if (code.isEmpty()) {
            throw new IllegalArgumentException("任务代码不能只含空白");
        }
        // 数量约束与其他创建入口共享,不读取尚未赋值的字段。
        if (copies 

这里先判空,再去空白,最后判断空字符串,是本例针对代码输入的策略。若协议要求保留首尾空格,就应该改为拒绝或原样保留;规范化不是通用修复器。也不要只调用 code.strip() 却丢掉返回值:这个例子需要让参数绑定到规范化后的字符串。

紧凑构造器参数、局部规则与组件字段的静态关系
图1:参数与字段结构说明图。参数组是体内校验和规范化的对象,字段组表示正常完成后的存储位置;连线只表示对应关系,不表示运行截图或执行流程。

我特别会避开 check(this) 这种写法。若 check 内部通过访问器读取组件,它看的不是当前参数值。应把参数传给静态辅助方法,例如传入 code 和 copies;辅助方法也不需要持有正在构造的对象。完整规范构造器可以显式赋字段,但它和紧凑构造器不能同时作为同一个 record 的规范构造器。

集合组件的复制与校验放在同一个入口

字符串与整数容易处理,集合更值得明确所有权。若只把外部 List 引用放进 final 字段,调用方仍可能修改原列表。我倾向于在紧凑构造器里获得不可修改列表,再检查本对象需要的元素规则;下面是另一个独立的 record 示例,可保存为 PrintBatch.java。

import java.util.List;
import java.util.Objects;

public record PrintBatch(List labels, int copies) {
    public PrintBatch {
        // copyOf 拒绝空元素,并使外部列表后续改动不影响这里保存的列表内容。
        labels = List.copyOf(Objects.requireNonNull(labels, "标签列表不能为空"));
        if (labels.isEmpty()) {
            throw new IllegalArgumentException("至少提供一个标签");
        }
        for (String label : labels) {
            // 本例拒绝空白标签,不擅自改变标签原文。
            if (label.isBlank()) {
                throw new IllegalArgumentException("标签不能只含空白");
            }
        }
        if (copies 

List.copyOf 的契约是不可修改列表,不保证每次创建新对象。输入为 null 或包含 null 元素时会失败;它保留元素引用,不提供深复制。示例使用 String,若换成可变的标签对象,还要为元素定义单独的快照策略。复制的遍历与存储成本也要纳入大批量输入的资源预算,不能把它宣传成没有成本的防护。

为了让错误提示稳定,可以把参数规则整理成表。表中异常来自这两份示例的具体写法,不代表所有业务必须使用相同异常类别。

输入情况处理位置示例的预期表现
代码为空引用参数判空NullPointerException
代码只有空白规范化后检查IllegalArgumentException
份数为零或负数数值规则IllegalArgumentException
列表含空引用元素List.copyOfNullPointerException
标签只有空白元素规则IllegalArgumentException

哪些逻辑应该留在 record 外部

把规则集中到构造器,不等于把服务层搬进去。对于同样的 code 和 copies,如果校验结果取决于网络是否可用、库存此刻是否变化,这个对象就有了额外的外部依赖。我的取舍是让 record 负责自身局部不变量,让服务负责授权、库存与事务判断,并由请求边界把异常转换为适当的用户提示。

原始输入、record 局部契约与业务服务职责的静态关系
图2:校验职责结构说明图。原始输入保留表单问题,record 约束局部数据,业务服务承接权限与库存;组间连线表示依赖或职责关联,并不表示库存检查具备事务保证。

比如“份数必须大于零”可以由构造器保证,“目前库存足够印制”还需要库存服务的并发控制。即便曾经查过库存,也不能据此保证后续提交一定成功。不要在构造器里预留库存、发送消息或写订单,这些副作用应由业务操作明确承担失败处理。

同样,构造器适合尽早拒绝一份不可接受的数据,却不天然提供“全部错误一起返回”的界面体验。原始输入模型可以保存用户输入与错误集合,服务完成必要的外部判断后再创建或使用 record。这种分工比在紧凑构造器里累积 UI 状态更容易说明。

改造后按输入契约观察结果

本文没有上线数据。对这样的改造,我关注的是调用方行为是否符合预期:所有常规创建路径是否使用同一构造入口,非法输入能否被边界层解释,原始列表变化是否影响已创建对象,以及新复制成本是否适合输入规模。

读者在自己的项目中可以安排几组对照输入:带首尾空白的任务代码、只含空白的代码、零份数、空标签列表、含 null 的列表,以及创建后再修改原始列表。对照目标是上文的契约,而不是比较某段固定控制台文本。并发修改输入集合需要调用方先提供稳定快照,构造器不是通用的并发快照设施。

我会先迁移约束少、字段含义明确的值对象,再处理需要兼容旧数据的入口。若历史数据允许零份数,不应悄悄让新构造器吞掉错误或自动改成一份;应明确迁移策略、旧数据解释和错误提示。对于字段需要跨多个交互阶段逐步填写的编辑模型,普通可变模型可能更符合任务。

有关构造器校验的三个问题

把校验写进访问器可以吗?

如果目标是拒绝非法对象,应在创建边界完成,而不是等每次读取才发现。访问器可以有明确的自定义行为,但不应承担补齐未完成构造的职责。

可以添加更短参数列表的构造器吗?

可以提供非规范构造器,但仍需按语言规则委托到其他构造器,最终抵达规范构造入口。默认参数也必须满足不变量,不能用重载绕开检查。这里不展开不同版本的一般构造器语法变化。

仅添加校验注解就会自动检查吗?

不要把注解当作这段 Java 代码的自动执行逻辑。若使用框架验证,必须了解对应框架何时触发验证、覆盖哪些创建路径。对于这里的纯 Java 示例,实际执行的是构造器中的显式检查。

进一步阅读:JLS 紧凑规范构造器、Oracle record 类指南、List.copyOf 官方契约。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>