如何实现链式调用的 Builder?需要注意什么?
简化版
链式 Builder 的关键是每个设置方法返回当前 Builder 对象,也就是 return this,最后通过 build() 创建目标对象。需要注意的是,链式调用不等于建造者模式,真正重要的是最终构建、校验和对象状态一致性。
详细版
链式 Builder 的基本写法是:
User user = User.builder()
.name("Tom")
.age(20)
.email("tom@example.com")
.build();
实现时通常有几个要点:
- 产品类构造方法设为
private,避免绕过 Builder; - Builder 保存构建过程中的临时字段;
- 每个 setter 风格方法返回
this; build()统一做参数校验;- 最终对象尽量设计成不可变对象,例如字段使用
final。
示例:
public class User {
private final String name;
private final int age;
private final String email;
private User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.email = builder.email;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String name;
private int age;
private String email;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public User build() {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name required");
}
if (age < 0) {
throw new IllegalArgumentException("age must not be negative");
}
return new User(this);
}
}
}
需要注意:Builder 一般不是线程安全对象,不建议多个线程共享同一个 Builder 实例。Builder 是构建阶段的临时对象,用完即丢。
完整版教学
一、链式调用为什么可读性更好
构造方法的问题是参数没有名字:
new User("Tom", 20, "tom@example.com", true, 3);
调用方必须记住每个参数的位置和含义。链式 Builder 把参数名写进方法名:
User.builder()
.name("Tom")
.age(20)
.email("tom@example.com")
.build();
这是一种“自描述代码”。哪怕不看构造方法签名,也能大概理解对象是怎么被构建出来的。
二、链式调用的实现本质
链式调用没有神秘机制,本质就是每个方法返回当前对象:
public Builder name(String name) {
this.name = name;
return this;
}
调用 builder.name("Tom").age(20) 时,name() 返回同一个 Builder,所以后面还能继续调用 age()。
关键不是语法,而是设计边界。Builder 保存的是“构建参数”,不是最终业务对象。最终业务对象应该由 build() 统一创建。
三、为什么建议搭配不可变对象
建造者模式和不可变对象天然契合。Builder 负责可变的构建过程,Product 负责稳定的最终状态。
例如:
private final String name;
private final int age;
字段是 final,对象创建完成后不能被随意改动。这样有几个好处:
- 对象状态更可靠;
- 多线程读取更安全;
- 不会出现半初始化对象;
- 校验逻辑可以集中在构建阶段。
如果 Product 仍然暴露大量 setter,Builder 的价值会被削弱,因为对象创建后仍然可能被改坏。
四、常见坑:把链式 setter 当 Builder
下面这种写法只是链式 setter:
User user = new User()
.setName("Tom")
.setAge(20);
它的缺点是对象从 new User() 开始就已经存在,但此时可能还是半初始化状态。如果中途抛异常,或者对象被提前传出去,就会产生状态不完整的问题。
Builder 更推荐:
User user = User.builder()
.name("Tom")
.age(20)
.build();
在 build() 之前,只是在配置构建器;在 build() 之后,才拿到合法对象。
五、链式语法背后的对象身份与收口点
每个配置方法返回同一个 Builder 引用,因此 name().age().email() 只是连续修改一个临时对象。真正的边界发生在 build():此时读取快照、校验并创建 Product;若链式方法直接返回 Product 或提前暴露它,半初始化风险仍在。
Builder b = User.builder()
b.name("Tom") -> 修改 b.name,返回 b
b.age(20) -> 修改 b.age,返回同一个 b
b.email("tom@example.com") -> 返回 b
b.build()
检查 name 非空、age>=0
new User(b 的已验证快照)
User 字段 final
Builder 后续修改不影响 User
链式只是入口,可读性来自命名方法
这条时间线把“可变构建阶段”和“稳定成品阶段”分开。Builder 可以反复接收参数,但 Product 只有在所有规则通过后才出现;若 Product 在第一步就被 new 出来再逐项修改,就仍然存在半初始化对象泄漏的窗口。
六、构建契约与对象不变量
| 评审维度 | 本题结论 |
|---|---|
| 必填信息 | name 等业务必填项在 build() 检查或分阶段 API 强制。 |
| 默认值 | 可选字段在 Builder 初始化,调用链只覆盖变化项。 |
| 跨字段规则 | 链式方法可做局部校验,组合规则在 build()。 |
| 可变引用处理 | Builder 到 Product 时复制集合/数组,避免后续链式修改穿透。 |
| Builder 生命周期 | 同一调用链局部持有;多线程不共享。 |
| Product 交付保证 | 只有 build 成功后才出现,创建后不再暴露 setter 破坏状态。 |
build() 不是形式上的结束标记,而是对象从“参数集合”变成“合法业务值”的原子边界。单字段输入可以提前拒绝明显错误,跨字段规则必须等信息齐全后判断,Product 私有构造器还应保留必要兜底,避免未来新增创建入口绕过不变量。
七、与构造器、JavaBean 和工厂的选择边界
- 字段少且全部必填时,短构造器或 record 通常比 Builder 更直接。
- 字段多、可选项多、同类型参数易错时,命名步骤能显著提升调用可读性。
- JavaBean setter 适合某些框架绑定,但对象可能在设置完成前就被观察到。
- Builder 关注“同一种复杂对象怎样组装”;工厂模式关注“创建哪一种产品实现”。
- 两者可以组合:工厂选择具体产品族或 Builder,Builder 再完成复杂组装。
- 经典 Director 只有在固定步骤序列需要复用或存在多种表示时才有价值。
- 链式
return this只是语法,校验、默认值、拷贝和收口才是设计语义。 - 若 Builder 比 Product 规则还复杂,应重新拆分对象职责,而不是继续堆方法。
只想要链式修改现有可变对象时可叫 Fluent API;要称 Builder,应体现独立构建阶段和最终交付。
八、交付前的代码与测试检查
- 缺少每个必填字段分别调用
build(),应得到明确且稳定的异常。 - 完全不设置可选字段,核对默认值来自唯一权威位置。
- 传入最小值、最大值和越界值,确认范围判断没有反向或 off-by-one。
- 构造两个互相冲突的字段组合,确认只在信息完整时执行跨字段校验。
- 传入集合或数组后修改原引用,已构建 Product 不应跟着变化。
- 若 getter 返回可变数据,再尝试修改返回值,内部状态仍应保持不变。
- 连续调用同一个 Builder 两次,确认语义是明确允许复制还是文档明确禁止复用。
- 两线程共享一个 Builder 做压力测试应被禁止或证明安全,不能靠偶然结果。
- 若使用 Lombok,检查生成代码、默认值、
@Singular、构造器可见性和框架兼容。 - 若迁移旧 API,比较默认值、异常类型、序列化字段和所有旧调用点行为。
记忆钩子:return this 让句子连起来,build() 才让对象合法地出生。
九、常见误区与追问
- 误区:链式 setter 与 Builder 完全相同。 前者常直接修改已经存在的 Product,没有独立收口。
- 误区:每个 Builder 方法都应返回新 Builder。 常见可变 Builder 返回 this;不可变 Builder 才会返回新实例。
- 误区:build 后修改 Builder 必然影响 Product。 正确快照/拷贝设计应让已建对象独立。
- 追问:自引用泛型何时需要? 继承层次中的 fluent Builder 为保持子类返回类型时常用。
- 追问:调用顺序如何强制? 可用分阶段 Step Builder 接口,但类型和复杂度会增加。
- 追问:链式调用调试困难怎么办? 可拆成局部 Builder 变量逐步设置,链式不是强制要求。
十、加强记忆
链式 Builder 的语法核心是 return this,设计核心是 build() 收口。链式只是让调用更顺,真正让代码可靠的是构建阶段可变、最终对象稳定、校验集中在最后一步。