Java Builder 构建者模式详解:不可变对象、Lombok 设计与使用边界
Builder 构建者模式详解:不可变对象、Lombok 设计与使用边界
一、先别急着背概念:Builder 模式到底想解决什么?
构建者模式(Builder Pattern)是一种 创建型设计模式。
但如果你现在只记住这句话,其实一点用都没有。
我们换一种更接地气的说法:
当一个对象“参数很多、组合很多、还希望不可变”时,Builder 就出现了。
二、没有 Builder,我们会遇到什么问题?
1. 构造函数爆炸(新手最先踩的坑)
假设我们要写一个 Computer 类:
public class Computer {
private String cpu;
private String ram;
private String storage;
private boolean hasGraphicsCard;
}
如果你用“传统构造函数”来解决,很快就会变成这样:
new Computer(cpu);
new Computer(cpu, ram);
new Computer(cpu, ram, storage);
new Computer(cpu, ram, storage, hasGraphicsCard);
👉 构造函数数量不断膨胀,维护成本极高。
这就是经典的 Telescoping Constructor(伸缩构造函数问题)。
2. Setter 看似灵活,其实隐患更多
很多新手会退一步,选择 setXxx():
Computer pc = new Computer();
pc.setCpu("i9");
pc.setRam("32GB");
问题是:
-
对象在构建过程中处于 不完整状态
-
无法保证“必填字段”
-
对象很难设计成 不可变(Immutable)
👉 对象什么时候才算“构建完成”?代码本身并不知道。
三、Builder 模式的核心思想(一句话版)
把“构建过程”从“最终对象”中拆出来。
-
构建过程:可以慢慢来、可以校验、可以修改
-
最终对象:一次性生成,生成后不再变化
这正是 Builder 模式的精髓。
四、Builder 模式的经典结构(先认识角色)
在完整的设计模式定义中,Builder 包含 4 个角色:
-
Product(产品类):最终要创建的复杂对象
-
Builder(抽象建造者):定义构建步骤
-
ConcreteBuilder(具体建造者):真正执行构建
-
Director(指挥者):控制构建顺序(了解即可)
👉 但在日常 Java 开发中,我们几乎不会写完整的四件套。
五、最常见、最实用的写法:静态内部类 Builder
这是你在真实项目中 99% 会看到的 Builder 形式。
1. 产品类(不可变)
public class Computer {
private final String cpu;
private final String ram;
private final String storage;
private final boolean hasGraphicsCard;
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
this.hasGraphicsCard = builder.hasGraphicsCard;
}
关键点:
-
构造器是 private
-
所有字段都是 final
-
外部无法直接
new Computer()
2. Builder:专门负责“拼装”的角色
public static class Builder {
private String cpu;
private String ram;
private String storage;
private boolean hasGraphicsCard = false;
public Builder setCpu(String cpu) {
this.cpu = cpu;
return this;
}
public Builder setRam(String ram) {
this.ram = ram;
return this;
}
public Builder setStorage(String storage) {
this.storage = storage;
return this;
}
public Builder setGraphicsCard(boolean hasGraphicsCard) {
this.hasGraphicsCard = hasGraphicsCard;
return this;
}
public Computer build() {
return new Computer(this);
}
}
}
Builder 的职责非常清晰:
-
暂存参数
-
支持链式调用
-
控制对象创建时机
3. 客户端调用(一眼就懂)
Computer pc = new Computer.Builder()
.setCpu("Intel i9")
.setRam("32GB")
.setStorage("2TB SSD")
.setGraphicsCard(true)
.build();
👉 读代码的人,只关心:我要什么配置。
六、Builder 和 build() 到底是什么关系?(重点理解)
Builder 是“过程”,build() 是“结果”。
1. Builder 是什么?
Builder = 构建过程的临时容器
-
它是可变的
-
它可以反复修改
-
它本身并不是最终对象
Order.builder()
.orderNo("NO123")
.remark("test");
👉 到这里为止:
-
对象还没创建
-
只是配置在 Builder 里
2. build() 是什么?
build() = 对象诞生的唯一入口
Order order = Order.builder()
.orderNo("NO123")
.build();
build() 通常负责:
-
参数校验(必填项)
-
调用私有构造器
-
返回不可变对象
3. 状态转换的本质
| 阶段 | 状态 |
|---|---|
| Builder | 可变(Mutable) |
| build() 之后 | 不可变(Immutable) |
这一步,是 Builder 模式最核心的价值。
七、为什么 Builder 非常适合不可变对象?
很多人第一次学 Builder 模式时,会觉得它只是**“让构造函数更好看”**。
但实际上,Builder 最核心的价值之一,是——
它天生就是为“不可变对象(Immutable Object)”服务的。
要理解这一点,我们需要先弄清楚一个关键问题:
1. 不可变对象,难在哪?
所谓不可变对象,指的是:
对象一旦创建完成,内部状态就不能再被修改。
典型特征包括:
-
所有字段都是
final -
没有
setter -
状态只能在构造阶段被一次性确定
public class User {
private final String name;
private final int age;
private final String email;
public User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
}
这种对象的好处非常明显:
-
线程安全
-
状态可预测
-
不会被“偷偷改掉”
但问题也很明显:
👉 构造函数会变得非常痛苦。
-
参数一多,构造函数就“爆炸”
-
参数顺序极易传错
-
可选参数处理非常别扭
2. Builder 把「构造过程」和「最终对象」分开了
Builder 的核心思想不是“换一种写法”,而是:
把“一步步设置参数的过程”,和“对象真正诞生的那一刻”彻底分离。
User user = User.builder()
.name("Tom")
.age(18)
.email("tom@example.com")
.build();
在这个过程中,其实发生了两件事:
-
Builder 是可变的
-
你可以一步步调用
name()、age() -
中间状态可以随意调整
-
-
最终的 User 是不可变的
-
只有在
build()时,才一次性创建User -
创建完成后,对象状态被“冻结”
-
👉 可变只存在于 Builder,永远不会泄漏到最终对象中。
3. build(),其实是一个「状态冻结点」
这是很多人忽略的一点。
build()方法,本质上是一个 “状态转换”:
可变的 Builder
↓ build()
不可变的 User
在 build() 之前:
-
参数可以缺失
-
参数可以反复修改
-
对象还不存在
在 build() 之后:
-
所有字段一次性赋值
-
所有
final字段被初始化 -
对象状态不再允许变化
public User build() {
return new User(name, age, email);
}
📌 这正是 Builder 和不可变对象“天然契合”的关键原因。
4. 没有 Builder,不可变对象会退化成什么样?
如果你不用 Builder,通常只能在这两种方案里选:
方案一:超长构造函数
new User("Tom", 18, "tom@example.com", true, false, null, ...);
- 可读性极差
- 参数顺序一旦错,Bug 极隐蔽
方案二:放弃不可变性,使用 setter
User user = new User();
user.setName("Tom");
user.setAge(18);
-
对象在“半成品状态”下暴露
-
线程安全、约束性全部丢失
👉 Builder 是唯一一个:
同时保住「不可变性」和「可读性」的方案。
5. 一个一句话总结(非常适合收尾)
Builder 允许我们用“可变的方式去构造”,却得到一个“不可变的结果”。
也正因为如此:
- Java 的不可变对象
- Lombok 的
@Builder - Java 记录类(
record) - 以及很多领域模型(Domain Model)
几乎都会天然地向 Builder 靠拢。
八、Lombok 简化构建者模式
在 Java 开发中,手动编写 Builder 已经很少见了。大多数开发者会使用 Lombok 库,只需一个注解即可自动生成:
@Builder
只需在类上加一个注解,Lombok 就会在编译时自动生成私有构造函数、静态内部 Builder 类以及所有的链式赋值方法。
@Getter // 1. 构建后通常只需要读取,属性应设为私有
@ToString // 2. 方便日志排查
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE) // 3. 重点:隐藏全参构造
public class Animal {
private final String name;
private final Integer age;
private final String color;
private final String type;
}
使用 @Builder 注解后,Lombok 会在编译时自动生成上述复杂的内部类代码。
测试@Builder
@Test
void contextLoads() {
Animal animal = Animal.builder()
.name("狗")
.age(20)
.color("黄色")
.type("狗类")
.build();
System.out.println(animal);
}
- 结果输出
Animal(name=狗, age=20, color=黄色, type=狗类)
@Builder(toBuilder=true):构建新对象
痛点解决:修改不可变对象 (
toBuilder = true)
正如之前提到的,如果你用了 final 关键字,对象就无法修改。开启 toBuilder 功能可以基于现有对象创建一个新的 Builder,从而实现“局部修改”。
@Getter // 1. 构建后通常只需要读取,属性应设为私有
@ToString // 2. 方便日志排查
@Builder(toBuilder = true) // 3. 允许从对象实例反向创建
@AllArgsConstructor(access = AccessLevel.PRIVATE) // 4. 重点:隐藏全参构造
public class Animal {
private final String name;
private final Integer age;
private final String color;
private final String type;
}
测试@Builder(toBuilder=true)
@Test
void contextLoads() {
Animal animal = Animal.builder()
.name("狗")
.age(20)
.color("黄色")
.type("狗类")
.build();
System.out.println(animal);
// 基于现有对象animal,创建Builder一个新的cat对象
Animal cat = animal.toBuilder()
// 只修改名字
.name("猫")
// 只修改类型
.type("猫类")
.build();
System.out.println("cat = " + cat);
}
- 结果输出:
只修改名字和类型,age和color保持不变
cat = Animal(name=猫, age=20, color=黄色, type=猫类)
@Builer.Default:默认值
这是新手最容易踩的坑。 在使用 @Builder 时,你在类成员变量上直接写的初始值(如 private int age = 18;)会被忽略,build() 出来的对象该属性会变成 0 或 null。
Lombok 允许你通过 @Builder.Default 注解为某些字段提供默认值。这样,当你没有为这些字段提供值时,构建器将使用默认值。
@Getter // 1. 构建后通常只需要读取,属性应设为私有
@ToString // 2. 方便日志排查
@Builder(toBuilder = true) // 3. 允许从对象实例反向创建
@AllArgsConstructor(access = AccessLevel.PRIVATE) // 4. 重点:隐藏全参构造
public class Animal {
private final String name;
@Builder.Default
private final Integer age = 18;
private final String color;
private final String type;
}
**测试 @Builder.Default **
Animal aDefault = Animal.builder().name("default").build();
System.out.println("aDefault = " + aDefault);
- 结果为:年龄age默认值:18
aDefault = Animal(name=default, age=18, color=null, type=null)
@Singular:集合属性
处理集合属性 (
@Singular)
手写 Builder 时,处理 List 或 Map 非常麻烦。Lombok 的 @Singular 注解可以让你一次只添加一个元素,且它会自动帮你处理不可变集合。
@Getter // 1. 构建后通常只需要读取,属性应设为私有
@ToString // 2. 方便日志排查
@Builder(toBuilder = true) // 3. 允许从对象实例反向创建 Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE) // 4. 重点:隐藏全参构造
public class Animal {
private final String name;
@Builder.Default
private final Integer age = 18;
private final String color;
private final String type;
@Singular
private final List<Integer> friends;
}
测试@Singular注解
Animal friend = Animal.builder()
.friends(List.of(1))
.friends(List.of(1,2))
.friends(List.of(3)).build();
System.out.println("friend = " + friend);
- 测试结果:
friend = Animal(name=null, age=18, color=null, type=null, friends=[1, 1, 2, 3])
@NonNull:字段必填
如果你使用了 Lombok,可以在字段上加上 @NonNull 注解。Lombok 会自动在生成的 Builder 方法和构造函数中插入空检查(Null Check)。
@Getter // 1. 构建后通常只需要读取,属性应设为私有
@ToString // 2. 方便日志排查
@Builder(toBuilder = true) // 3. 允许从对象实例反向创建 Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE) // 4. 重点:隐藏全参构造
public class Animal {
private final String name;
@Builder.Default
private final Integer age = 18;
private final String color;
private final String type;
@Singular
private final List<Integer> friends;
@NonNull
private final Long id;
}
id字段为Null,报错信息为:
java.lang.NullPointerException: id is marked non-null but is null
测试@NonNull代码:
Animal animal = Animal.builder()
.name("狗")
.age(20)
.color("黄色")
.type("狗类")
.build();
System.out.println(animal);
- 结果为:
Animal(name=狗, age=20, color=黄色, type=狗类, friends=[], id=1)
九、什么时候该用 Builder?
Builder 不是“写代码更高级”的象征,也不是所有对象都值得用 Builder。
真正的问题只有一个:
👉 这个对象的“创建过程”复杂吗?
如果复杂,Builder 很合适;
如果不复杂,Builder 反而是负担。
下面我们从 「什么时候该用」 和 「什么时候不该用」 两个方向来判断。
(一)这 5 种场景,非常适合用 Builder
1. 构造参数多(尤其是 3 个以上)
当一个类的构造参数开始变多时,构造函数的可读性会急剧下降:
new User("Tom", 18, "tom@example.com", "Beijing", true);
你很难一眼看出:
"Beijing"是地址还是城市?true代表什么含义?
使用 Builder 后:
User user = User.builder()
.name("Tom")
.age(18)
.email("tom@example.com")
.city("Beijing")
.vip(true)
.build();
👉 参数“自带语义”,几乎不会传错。
2. 参数有明显的「可选项」
当一个对象:
-
有些字段经常不用
-
有些字段有默认值
-
不同场景需要不同组合
Builder 非常合适。
User user = User.builder()
.name("Tom")
.build(); // 只关心必须字段
👉 不需要为了“少传几个参数”,去写多个构造函数。
3. 对象希望设计成「不可变」
如果你希望对象:
-
字段都是
final -
没有
setter -
创建后状态不可更改
👉 Builder 几乎是标配方案。
@Builder
public class Order {
private final String orderNo;
private final BigDecimal amount;
}
Builder 负责“可变构建”,对象本身保持“不可变”。
4. 创建时需要做校验或约束
有些对象在创建时就需要保证合法性:
-
必填字段不能为空
-
数值范围合法
-
字段之间存在约束关系
public Order build() {
if (orderNo == null) {
throw new IllegalStateException("orderNo is required");
}
return new Order(orderNo, amount);
}
👉 Builder 是“创建规则的守门人”。
5. 对象创建逻辑可能演进、扩展
当你预期这个类:
-
将来可能新增字段
-
构造规则可能变化
-
不想破坏已有代码
Builder 非常稳妥:
-
新增字段 → 新增一个链式方法
-
老代码不受影响
👉 对扩展友好,对修改不敏感。
(二)这 3 种场景,不建议用 Builder
Builder 很好,但不是银弹。
1. 对象非常简单
new Point(10, 20);
-
字段少
-
语义明确
-
没有可选项
👉 用 Builder 只会增加噪音。
2. 对象只是临时数据载体(DTO / VO)
如果一个类:
-
只是用来传参
-
没有业务约束
-
生命周期很短
public class LoginDTO {
public String username;
public String password;
}
👉 用 Builder 没有明显收益。
3. 对象必须频繁、大量创建(性能敏感)
Builder 会多创建一个 Builder 实例。
在以下场景要慎用:
-
高频循环
-
性能极度敏感的底层代码
-
大量对象批量创建
👉 此时简单构造函数更直接。
(三)新手最容易踩的 3 个误区
误区一:所有类都用 Builder,看起来更“高级”
❌ 错。
设计模式是为了解决问题,而不是装饰代码。
误区二:有 Builder 就不需要构造函数设计了
❌ 错。
- 构造函数依然是“最终入口”
- Builder 只是调用它的“代理人”
误区三:Builder = 工厂模式
❌ 错。
- Builder 关注的是 “如何一步步构建”
- 工厂关注的是 “创建哪一种对象”
(四)一句「使用判断口诀」(强烈建议保留)
参数多、可选多、想不可变、要校验、怕扩展 —— 用 Builder。
简单对象、临时数据、高频创建 —— 别用 Builder。
十、给新手的最终总结
Builder 模式不是为了炫技,而是为了让“复杂对象的创建”这件事变得可控、可读、可维护。
如果你只记住一句话:
Builder 管过程,build() 给结果。
这篇文章的目的就达到了。
更多推荐



所有评论(0)