← 返回题目列表

Lombok 的 @Builder 和手写建造者有什么区别?

高频 中等 第 10 / 26 题 更新于 2026/08/03
建造者模式Lombok@BuilderJava

简化版

Lombok 的 @Builder 可以自动生成 Builder 样板代码,适合字段较多但构建规则不复杂的对象。手写 Builder 更适合需要复杂校验、默认值、防御性拷贝、构建步骤控制或更强可读性的场景。

详细版

@Builder 的优势是减少样板代码:

@Builder
public class UserQuery {
    private Long userId;
    private String status;
    private Integer pageNo;
    private Integer pageSize;
}

调用时:

UserQuery query = UserQuery.builder()
        .userId(1001L)
        .status("ACTIVE")
        .pageNo(1)
        .pageSize(20)
        .build();

它的优点:

  • 代码少;
  • 调用可读性好;
  • 适合 DTO、查询条件、简单配置对象;
  • 和 Lombok 的 @Getter@Value 等注解搭配方便。

但它也有局限:

  • 默认值需要配合 @Builder.Default,否则字段初始化值可能不按预期生效;
  • 复杂校验不如手写 Builder 清晰;
  • 对集合字段要注意 @Singular 和不可变处理;
  • 生成代码隐藏在编译期,新人排查问题可能不直观;
  • 过度使用可能让领域对象的构建规则变得隐式。

面试回答时可以说:@Builder 是工程效率工具,不是模式理解的替代品。简单对象可以用 Lombok,复杂对象建议手写关键构建逻辑。

完整版教学

一、Lombok @Builder 到底做了什么

@Builder 本质上是在编译期帮你生成一个 Builder 类。它会生成类似下面的代码:

public static UserQueryBuilder builder() {
    return new UserQueryBuilder();
}

public static class UserQueryBuilder {
    private Long userId;
    private String status;

    public UserQueryBuilder userId(Long userId) {
        this.userId = userId;
        return this;
    }

    public UserQuery build() {
        return new UserQuery(userId, status);
    }
}

所以它不是一种新模式,而是帮我们少写建造者模式的样板代码。

理解这一点很重要。面试官问 Lombok Builder 时,通常不是想听“加个注解就行”,而是想看你是否知道它生成了什么、适合什么、不适合什么。

二、默认值是高频坑

很多人会写:

@Builder
public class ClientConfig {
    private int timeoutMs = 3000;
}

然后以为:

ClientConfig config = ClientConfig.builder().build();

timeoutMs 会是 3000。实际使用 Lombok 时,这类默认值需要特别注意,通常要配合 @Builder.Default

@Builder
public class ClientConfig {
    @Builder.Default
    private int timeoutMs = 3000;
}

原因是 Lombok 生成的 Builder 会持有自己的字段,构建时使用 Builder 字段生成对象;普通字段初始化不一定等价于 Builder 默认值。

这个点在面试中很容易加分,因为它说明你不是只会贴注解。

三、复杂校验为什么手写更好

如果只是查询条件,@Builder 很舒服:

OrderQuery.builder().status("PAID").pageSize(20).build();

但如果对象有复杂规则,手写 Builder 更清楚。例如:

  • 开启 TLS 必须提供证书;
  • 重试次数和超时时间必须匹配;
  • 起止时间必须同时存在且开始时间小于结束时间;
  • 集合字段需要不可变拷贝;
  • 某些字段只能二选一。

这些规则虽然也可以通过自定义构造器或额外方法处理,但如果规则很多,隐藏在 Lombok 生成代码周围反而会降低可读性。

手写 Builder 可以把规则明确写在 build()

public ClientConfig build() {
    if (tlsEnabled && certificate == null) {
        throw new IllegalStateException("certificate required");
    }
    return new ClientConfig(this);
}

这段代码让构建规则一眼可见。

四、Lombok Builder 的使用边界

比较稳的工程经验是:

  • 简单 DTO、查询条件、测试对象:可以用 @Builder
  • 核心领域对象、复杂配置对象、涉及不变量的对象:优先手写;
  • 对外 SDK 或公共组件:谨慎使用 Lombok,避免调用方理解成本和依赖约束;
  • 集合字段和默认值:一定要单独检查。

如果团队大量使用 Lombok,要保证 IDE 插件、编译配置、代码规范都一致,否则新人会看到“源码里没有方法,但代码能调用”的魔法感。

五、展开生成代码检查默认值与集合语义

评审 @Builder 时应像看手写代码一样查看 delombok 结果:Builder 有独立字段和“是否显式设置”标记,普通字段初始化不等于 Builder 默认值;集合的 @Singular 又会生成逐项收集与构建期不可变处理。

源码: @Builder class ClientConfig
编译期生成 ClientConfigBuilder
builder.timeoutMs 字段有自己的初始语义
未加 @Builder.Default 时普通字段初值可能不进入 Builder 路径
加 @Builder.Default 后生成 set 标记与默认提供方法
@Singular("header") 生成 header(k,v)/headers(map)
build() 汇总集合
复杂跨字段规则仍需显式代码
delombok/IDE 可查看真实生成结果
测试 builder().build() 的默认对象

这条时间线把“可变构建阶段”和“稳定成品阶段”分开。Builder 可以反复接收参数,但 Product 只有在所有规则通过后才出现;若 Product 在第一步就被 new 出来再逐项修改,就仍然存在半初始化对象泄漏的窗口。

六、构建契约与对象不变量

评审维度本题结论
必填信息Lombok 不会替业务自动定义必填字段,应通过自定义构造/校验表达。
默认值需要 Builder 路径默认值时使用 @Builder.Default 并用测试确认版本行为。
跨字段规则复杂不变量应放显式构造器、工厂或自定义 build 附近,不能假设注解生成。
可变引用处理集合结合 @Singular 仍需确认元素可变性和所需深拷贝。
Builder 生命周期生成的 Builder 仍是普通可变对象,不自动线程安全。
Product 交付保证是否不可变取决于字段、构造与暴露 API,不取决于单独 @Builder

build() 不是形式上的结束标记,而是对象从“参数集合”变成“合法业务值”的原子边界。单字段输入可以提前拒绝明显错误,跨字段规则必须等信息齐全后判断,Product 私有构造器还应保留必要兜底,避免未来新增创建入口绕过不变量。

七、与构造器、JavaBean 和工厂的选择边界

  • 字段少且全部必填时,短构造器或 record 通常比 Builder 更直接。
  • 字段多、可选项多、同类型参数易错时,命名步骤能显著提升调用可读性。
  • JavaBean setter 适合某些框架绑定,但对象可能在设置完成前就被观察到。
  • Builder 关注“同一种复杂对象怎样组装”;工厂模式关注“创建哪一种产品实现”。
  • 两者可以组合:工厂选择具体产品族或 Builder,Builder 再完成复杂组装。
  • 经典 Director 只有在固定步骤序列需要复用或存在多种表示时才有价值。
  • 链式 return this 只是语法,校验、默认值、拷贝和收口才是设计语义。
  • 若 Builder 比 Product 规则还复杂,应重新拆分对象职责,而不是继续堆方法。

DTO、测试数据和简单查询对象适合 Lombok;核心领域对象、复杂校验和公共 SDK 优先显式手写规则。

八、交付前的代码与测试检查

  1. 缺少每个必填字段分别调用 build(),应得到明确且稳定的异常。
  2. 完全不设置可选字段,核对默认值来自唯一权威位置。
  3. 传入最小值、最大值和越界值,确认范围判断没有反向或 off-by-one。
  4. 构造两个互相冲突的字段组合,确认只在信息完整时执行跨字段校验。
  5. 传入集合或数组后修改原引用,已构建 Product 不应跟着变化。
  6. 若 getter 返回可变数据,再尝试修改返回值,内部状态仍应保持不变。
  7. 连续调用同一个 Builder 两次,确认语义是明确允许复制还是文档明确禁止复用。
  8. 两线程共享一个 Builder 做压力测试应被禁止或证明安全,不能靠偶然结果。
  9. 若使用 Lombok,检查生成代码、默认值、@Singular、构造器可见性和框架兼容。
  10. 若迁移旧 API,比较默认值、异常类型、序列化字段和所有旧调用点行为。

记忆钩子:@Builder 省的是样板,不会替你发明默认值、不变量和所有权。

九、常见误区与追问

  • 误区:字段声明处的初值一定会成为 Builder 默认值。 Lombok Builder 路径通常需 @Builder.Default 才表达该语义。
  • 误区:加 @Builder 后对象自动不可变。 仍取决于字段 final、setter 和可变引用拷贝。
  • 误区:@Singular 等于深拷贝集合元素。 它主要处理容器构建,元素自身可变性仍存在。
  • 追问:如何知道 Lombok 生成了什么? 使用 delombok、IDE 结构视图或查看编译产物。
  • 追问:继承对象用什么? @SuperBuilder 可支持层次,但父子配置和版本兼容更复杂。
  • 追问:为什么公共库要谨慎? 生成 API、注解处理依赖和二进制兼容会成为对外契约的一部分。

十、加强记忆

@Builder 是省代码的工具,手写 Builder 是表达规则的设计。简单对象用 Lombok 提效,复杂对象把规则写清楚;尤其记住默认值、集合字段和复杂校验这三个坑。