引子:两段看似一样的代码
1
2
3
4
5
| // 写法 A
User user = new User();
user.setName("张三");
user.setAge(20);
user.setEmail("zhangsan@example.com");
|
1
2
3
4
5
6
| // 写法 B
User user = User.builder()
.name("张三")
.age(20)
.email("zhangsan@example.com")
.build();
|
1
2
3
4
5
| // 写法 C
User user = new User()
.setName("张三")
.setAge(20)
.setEmail("zhangsan@example.com");
|
B 和 C 都是"链式调用",看起来差不多——但它们解决的是完全不同的问题。B 是Builder 模式,C 是Fluent Setter 风格。两者在中文社区经常被混为一谈,导致很多文章在讨论"Builder 模式"时其实展示的是 Fluent。
本文把这两个概念彻底分清楚,讲清楚各自解决什么问题、什么时候用哪个、配合 Lombok 怎么写最舒服。
一、先把概念分清楚
Fluent(流式接口)
Fluent 是一种接口设计风格——方法返回 this(或某种上下文对象),让调用可以连成一串。它本身不是设计模式,只是一种"让 API 读起来像句子"的写法。
1
2
3
4
5
6
7
8
| public class StringBuilder {
public StringBuilder append(String s) {
// ...
return this; // ← 关键
}
}
new StringBuilder().append("Hello, ").append("world").append("!");
|
JDK 里 StringBuilder、Stream 都是 Fluent 风格。
Builder(生成器模式)
Builder 是 GoF 设计模式——为构造一个复杂对象单独建一个"建造者"对象,把"对象的构造过程"和"对象本身"分开。
1
2
3
4
| User u = new User.Builder()
.name("张三")
.age(20)
.build(); // ← 关键:build() 才返回真正的目标对象
|
Builder 通常也用 Fluent 风格写——所以才容易混淆。但它的核心特征不是"链式",而是:
- 构造过程和目标对象分离(中间态在 Builder 上,不污染目标对象)
- 有一个明确的
build() 终点 - 目标对象通常是不可变的(构造完所有字段都 final)
二、Fluent Setter:最朴素的链式
最简单的 Fluent 写法——在 Setter 里返回 this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public class User {
private String name;
private Integer age;
public User setName(String name) {
this.name = name;
return this; // ← Fluent 关键
}
public User setAge(Integer age) {
this.age = age;
return this;
}
}
new User()
.setName("张三")
.setAge(20);
|
Lombok 一个 @Accessors(chain = true) 就能生成:
1
2
3
4
5
6
| @Data
@Accessors(chain = true)
public class User {
private String name;
private Integer age;
}
|
或者去掉 set 前缀让 API 更紧凑:
1
2
3
4
5
6
7
| @Accessors(fluent = true)
public class User {
private String name;
private Integer age;
}
new User().name("张三").age(20);
|
优点
- 写法简单,对原 POJO 改动最小
- API 紧凑,链式
- 容易加在已有项目上,渐进式改造
缺点
- 可变对象——构造完之后还能改,线程安全性差
- 没有"构造完成"的明确语义——任何时候都可以接着调
- 不能做必填校验——你
new User() 之后直接用,编译期不会报错 - 和 JavaBean 规范冲突——
set 系列方法返回 void 是 JavaBean 标准,框架(早期 BeanUtils、序列化库)可能不识别
所以——Fluent Setter 更适合内部 POJO,对外 API 通常优先选 Builder 这种语义更明确的风格。
三、Builder 模式:标准写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| public class User {
private final String name;
private final Integer age;
private final String email;
private User(Builder b) {
this.name = b.name;
this.age = b.age;
this.email = b.email;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String name;
private Integer age;
private String email;
public Builder name(String name) { this.name = name; return this; }
public Builder age(Integer age) { this.age = age; return this; }
public Builder email(String e) { this.email = e; return this; }
public User build() {
// 必填校验放这里
Objects.requireNonNull(name, "name is required");
return new User(this);
}
}
// 只暴露 getter,没有 setter
public String getName() { return name; }
public Integer getAge() { return age; }
public String getEmail() { return email; }
}
|
调用方:
1
2
3
4
5
| User u = User.builder()
.name("张三")
.age(20)
.email("z@x.com")
.build();
|
Lombok 一个 @Builder 就能生成上面所有的代码:
1
2
3
4
5
6
7
| @Builder
@Getter
public class User {
private final String name;
private final Integer age;
private final String email;
}
|
优点
- 目标对象不可变——所有字段
final,天然线程安全 build() 是明确终点——调用方知道"这里之前在配置,之后是真正的对象"- 必填校验集中——所有约束放
build() 一处 - 构造逻辑可独立演化——Builder 可以加链式方法、衍生 API,目标对象保持简洁
- 天然适合"参数多且大部分可选"的对象
缺点
- 类多一倍(一个目标 + 一个 Builder),不用 Lombok 写起来烦
- 字段加减时 Builder 也要改
四、什么时候用 Fluent,什么时候用 Builder
用 Fluent Setter 的场景
- 进程内的 DTO / VO / Form——只在内部传递,不需要不可变性
- 已有 POJO 的最小改造——加个
@Accessors(chain=true) 就完事 - 配置类、上下文对象——会被持续修改,本就是可变的
用 Builder 的场景
- 不可变值对象——领域模型、配置对象、消息体
- 构造参数多(≥4 个)且大部分可选——Builder 比"重载 N 个构造器"清爽得多
- 对外暴露的 API——Builder 给调用方明确的"配置 → 完成"语义
- 需要必填校验——
build() 是兜底
两者结合的反面教材
1
2
3
4
5
6
7
| // 反面教材:既能 builder().build(),又能直接 new + setter
@Data
@Builder
public class User {
private String name;
private Integer age;
}
|
@Data 会生成 setter,@Builder 会生成 Builder——结果就是这个对象既不可变,又有 Builder。Builder 的不可变优势完全丧失。
正确写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 不可变 + Builder
@Builder
@Getter // 只生成 getter,没有 setter
public class User {
private final String name;
private final Integer age;
}
// 或者:可变 + Fluent,不要 Builder
@Data
@Accessors(chain = true)
public class UserDTO {
private String name;
private Integer age;
}
|
两种风格通常不建议混在一个类上——除非你确实需要可变 + Builder 双重能力,否则会让语义模糊。
五、Builder 进阶用法
必填字段强制:阶梯式 Builder(Step Builder)
普通 Builder 只能在 build() 里运行时校验,编译期帮不了你。Step Builder 通过类型系统让"必填字段没填就编译不过":
1
2
3
4
5
6
| User u = User.builder()
.name("张三") // 必填
.age(20) // 必填
.optional() // 进入"可选阶段"
.email("...")
.build();
|
实现要写多个接口(NameStep → AgeStep → OptionalStep),代码量大,只有"对外 SDK,且想强制必填"时才值得用。
Builder 里加业务校验
1
2
3
4
5
6
7
8
9
| public User build() {
if (age != null && age < 0) {
throw new IllegalArgumentException("age must be >= 0");
}
if (email != null && !email.contains("@")) {
throw new IllegalArgumentException("invalid email");
}
return new User(this);
}
|
所有"对象不变式"应当在 build() 里检查,让目标对象一旦构造出来就一定是合法的。
Builder 复用:从已有对象出发再修改
可变对象修改方便,不可变对象怎么办?答案是 toBuilder:
1
2
3
4
5
6
7
8
9
| @Builder(toBuilder = true)
@Getter
public class User {
private final String name;
private final Integer age;
}
User u1 = User.builder().name("张三").age(20).build();
User u2 = u1.toBuilder().age(21).build(); // 创造一个"修改了 age 的副本"
|
toBuilder() 把现有对象的所有字段拷进 Builder,让你像"对象修改"一样,但底层每次都是创建新对象——既保留了不可变,又方便"小改造"。
集合字段:@Singular
参数里有集合的话,Lombok 提供 @Singular:
1
2
3
4
5
6
7
8
9
10
11
12
13
| @Builder
public class Order {
private final String orderNo;
@Singular
private final List<OrderItem> items;
}
Order o = Order.builder()
.orderNo("123")
.item(item1) // 单个元素
.item(item2)
.items(List.of(...))// 批量
.build();
|
六、Fluent 的另外两个高级形态
1. Method Chaining 操作集合:Stream
1
2
3
4
5
| List<String> result = users.stream()
.filter(u -> u.getAge() > 18)
.map(User::getName)
.sorted()
.collect(toList());
|
Stream 的每一步都返回 Stream<T>,看起来像 Fluent,但本质是返回新对象而非 this。这是 Fluent 的"不可变变体"——每个操作产生新流,原流不变。这种"Fluent + 不可变"的组合在函数式风格里非常常见。
2. 内部 DSL:建造一棵描述树
很多框架用 Fluent 风格搭出一个"Domain Specific Language":
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // MyBatis-Plus QueryWrapper
new LambdaQueryWrapper<User>()
.eq(User::getStatus, 1)
.like(User::getName, "张")
.orderByDesc(User::getCreateTime);
// Mockito
when(repo.findById(1L))
.thenReturn(Optional.of(user));
// AssertJ
assertThat(users)
.hasSize(2)
.extracting(User::getName)
.containsExactly("张三", "李四");
|
Fluent 的真正威力在这里——让代码读起来像自然语言/SQL,远超普通 setter 链能做到的。
七、踩坑提醒
@Data + @Builder 反模式——前面说过,要么不可变 + Builder,要么可变 + Fluent,别叠加@SuperBuilder 用于继承——@Builder 不支持父子类,要用 @SuperBuilder- Fluent Setter 和 JavaBean 规范冲突——一些老库(如 Apache BeanUtils)按"setter 返回 void"识别,看不到 Fluent setter;序列化库通常没问题(用反射)
- 链式调用的报错堆栈不友好——一行长链报错时只能看到行号,建议合理换行
- 不要做"链式校验"——把
validate() 加进链里看似优雅,实际让链式表达力变差,用 build() 集中校验 - 不可变对象的
equals/hashCode——使用 @Value 或者 @EqualsAndHashCode,否则 Map / Set 行为会出错
八、一个工程上的"风格指南"
我自己在团队里推的一份简版规则:
| 类型 | 推荐风格 |
|---|
| 领域模型 / 值对象 | @Builder + @Getter,不可变 |
| API 请求/响应 DTO | @Data + @Accessors(chain=true),可变 + Fluent |
| 配置类 | @Builder + @Getter,不可变 |
| 内部 DTO/VO | 视情况用 Fluent,不需要 Builder |
| 框架配置/上下文 | 视情况用 Builder,明确"build 完成"语义 |
把"什么对象用什么风格"约定下来,比"代码漂亮"重要得多。
小结
把全文压一句:
Fluent 是 API 风格——让代码读起来像句子;Builder 是设计模式——让复杂对象的构造和不可变共存。前者解决可读性,后者解决可维护性。
记住三个判断:
- 要不要不可变? 要 → Builder;不要 → Fluent Setter
- 构造参数多不多? 多(≥4)→ Builder;少 → 普通构造器
- 是不是对外 API? 是 → Builder(明确语义);否 → 都行
把概念拎清楚,下次再看到 obj.a().b().c().build() 这种代码,你就能一眼看出它是真 Builder 还是 Fluent 冒充的,也就能写出更精准的 API。