建造者(Builder)是创建型模式里"看起来最简单、实际最容易用错"的一个。它有两种截然不同的形态——GoF 经典建造者(关注"分步装配、同一过程产出不同表示")和流式建造者(关注"把一堆可选参数说得清楚"),工程里 90% 的
XxxBuilder其实是后者。这篇文章把两种形态讲清、给出可运行代码,并说明为什么《Effective Java》只在参数 ≥ 4 个时才推荐它。
一、问题的起点:构造器参数爆炸
Java 没有具名参数和默认参数,于是当一个对象有"2 个必填 + 3 个可选"字段时,最朴素的写法会变成这样:
import java.util.List;
/** 伸缩式构造器(telescoping constructor):能跑,但没人愿意读 */
public class TelescopingConstructorDemo {
static class Order {
private final String id; // 必填
private final String customer; // 必填
private final String coupon; // 可选
private final boolean giftWrap; // 可选
private final String remark; // 可选
Order(String id, String customer) {
this(id, customer, null);
}
Order(String id, String customer, String coupon) {
this(id, customer, coupon, false);
}
Order(String id, String customer, String coupon, boolean giftWrap) {
this(id, customer, coupon, giftWrap, null);
}
Order(String id, String customer, String coupon, boolean giftWrap, String remark) {
this.id = id;
this.customer = customer;
this.coupon = coupon;
this.giftWrap = giftWrap;
this.remark = remark;
}
}
public static void main(String[] args) {
// 每个参数是什么意思?只能靠数位置,而且 true 是"礼品包装"还是"加急"?
Order order = new Order("A-1", "张三", null, true, null);
System.out.println(order.id);
}
}
三个后果:
- 调用处不可读:
new Order("A-1", "张三", null, true, null)里的null/true是什么?必须翻回去看构造器签名。 - 重载组合失控:可选参数越多,重载构造器数量越接近指数增长。真要写全,需要 2ⁿ 个构造器。
- 或者退化成 setter:一旦改用无参构造 +
setXxx(),对象就有了"半成品状态"——谁也不敢保证submit()拿到的order字段是齐的;同时还牺牲了不可变性。
建造者模式正是为这三件事而生。注意它的价值不在"多一层",而在"让可选参数有名字、让对象只在构建完成后才存在"。
二、两种"建造者"
同一个词,工程里指两种不同的东西,先把它们分开:
| 形态 | 关注点 | 典型形态 | 出处 |
|---|---|---|---|
| 经典 GoF 建造者 | 分步装配的过程;同样的构建过程可以产出不同表示 | Director + 抽象 Builder + 多个 ConcreteBuilder | GoF《设计模式》 |
| 流式建造者(Fluent Builder) | 参数的可读性与对象的不变性;解决构造器参数过多 | Order.builder(...).item(...).build() | 《Effective Java》Item 2 |
两者的共同点是"把构建过程拆成若干步",区别在于为什么要拆:前者是为了让"过程"可复用(换个 ConcreteBuilder 就换一种产出),后者是为了让"参数"能起名字。混着理解是很多文章讲不清建造者的根源。
三、经典 GoF 建造者:同一过程,不同表示
经典场景是"文档导出":报表结构固定(标题 → 若干章节 → 页脚),但输出格式可能是 Markdown,也可能是 HTML。结构(过程)不变,表示(产物)可变。
import java.util.ArrayList;
import java.util.List;
/** 经典 GoF 建造者:分离"构建过程"与"表示" */
public class ClassicBuilderDemo {
/** Product:被构建的复杂对象 */
static class Document {
private final List<String> lines = new ArrayList<>();
void addLine(String line) {
lines.add(line);
}
String render() {
return String.join("\n", lines);
}
}
/** Builder:只声明"每一步做什么",不关心最终长什么样 */
abstract static class DocumentBuilder {
protected final Document doc = new Document();
Document result() {
return doc;
}
abstract void buildTitle(String title);
abstract void buildSection(String heading, String body);
abstract void buildFooter(String footer);
}
/** ConcreteBuilder A:Markdown 表示 */
static class MarkdownBuilder extends DocumentBuilder {
void buildTitle(String title) { doc.addLine("# " + title); }
void buildSection(String heading, String body) {
doc.addLine("## " + heading);
doc.addLine(body);
}
void buildFooter(String footer) { doc.addLine("---\n" + footer); }
}
/** ConcreteBuilder B:HTML 表示 */
static class HtmlBuilder extends DocumentBuilder {
void buildTitle(String title) { doc.addLine("<h1>" + title + "</h1>"); }
void buildSection(String heading, String body) {
doc.addLine("<h2>" + heading + "</h2>");
doc.addLine("<p>" + body + "</p>");
}
void buildFooter(String footer) { doc.addLine("<hr/><small>" + footer + "</small>"); }
}
/** Director:封装"固定的构建过程",它只知道 Builder 抽象 */
static class ReportDirector {
Document build(DocumentBuilder builder) {
builder.buildTitle("季度经营简报");
builder.buildSection("营收", "同比增长 12%");
builder.buildSection("风险", "供应链交付周期延长");
builder.buildFooter("机密 · 内部使用");
return builder.result();
}
}
public static void main(String[] args) {
ReportDirector director = new ReportDirector();
System.out.println("=== Markdown 表示 ===");
System.out.println(director.build(new MarkdownBuilder()).render());
System.out.println("\n=== HTML 表示 ===");
System.out.println(director.build(new HtmlBuilder()).render());
}
}

四个角色对应得很清楚:
- Product(产品):
Document,被构建的复杂对象; - Builder(抽象建造者):
DocumentBuilder,声明构建的每一步; - ConcreteBuilder(具体建造者):
MarkdownBuilder/HtmlBuilder,决定每一步"长什么样",并负责最终产出; - Director(指挥者):
ReportDirector,固定构建顺序,且只依赖抽象。
关键在于 Director 完全不知道产物格式——它调用的三个方法名是抽象的。这就是"同样的构建过程可以创建不同的表示"。

什么时候真的需要 Director?当"构建步骤的顺序"本身就是业务规则(先校验、再装配、最后封口)。如果只有一个 ConcreteBuilder、且步骤调用方自己很清楚,Director 就是多余的一层——这也是经典建造者被诟病"过重"的原因。
四、流式建造者:工程里最常见的形态
日常写代码时遇到的 XxxBuilder,几乎都是下面这种:私有构造器 + 静态 builder() + 链式方法 + build() 一次性校验。
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** 流式建造者:让可选参数有名字,让对象构造完成后就不可变 */
public class FluentBuilderDemo {
static final class Order {
private final String id; // 必填
private final String customer; // 必填
private final List<String> items; // 必填(至少一件)
private final String coupon; // 可选,默认 ""
private final boolean giftWrap; // 可选,默认 false
/** 私有:外部只能通过 Builder 创建,构造器同时承担"不变量校验" */
private Order(Builder b) {
if (b.id == null || b.id.isBlank()) {
throw new IllegalStateException("id 不能为空");
}
if (b.items.isEmpty()) {
throw new IllegalStateException("订单至少需要一件商品");
}
this.id = b.id;
this.customer = b.customer;
this.items = List.copyOf(b.items); // 防御性拷贝 + 不可变视图
this.coupon = b.coupon;
this.giftWrap = b.giftWrap;
}
/** 静态入口:必填参数在这里收,可选参数交给链式方法 */
static Builder builder(String id, String customer) {
return new Builder(id, customer);
}
List<String> items() {
return items;
}
@Override
public String toString() {
return "Order{id=" + id + ", customer=" + customer + ", items=" + items
+ ", coupon='" + coupon + "', giftWrap=" + giftWrap + "}";
}
static final class Builder {
private final String id; // 必填:创建 Builder 时就固定
private final String customer;
private final List<String> items = new ArrayList<>();
private String coupon = ""; // 可选:给默认值
private boolean giftWrap = false;
private Builder(String id, String customer) {
this.id = id;
this.customer = customer;
}
Builder item(String name) {
items.add(name);
return this;
}
Builder coupon(String code) {
this.coupon = code == null ? "" : code;
return this;
}
Builder giftWrap() {
this.giftWrap = true;
return this;
}
Order build() {
return new Order(this); // 校验与拷贝都发生在这一步
}
}
}
public static void main(String[] args) {
Order o1 = Order.builder("A-1", "张三")
.item("机械键盘")
.item("显示器")
.giftWrap()
.build();
System.out.println(o1);
Order o2 = Order.builder("A-2", "李四").item("鼠标").coupon("SUMMER").build();
System.out.println(o2);
try {
Order.builder("A-3", "王五").build(); // 忘了加商品
} catch (IllegalStateException e) {
System.out.println("构建失败:" + e.getMessage());
}
}
}

这套写法解决了第一节的三个问题:
- 可选参数有名字:
.giftWrap()比new Order(..., true, null)好读得多; - 对象只在
build()之后才存在,不存在"半成品"状态; - 最终对象字段全
final、集合不可变,天然线程安全、可放心共享。
五个值得坚持的细节
- 必填参数放在
builder(...)或Builder构造器里——用类型系统表达"必填",而不是靠build()里抛异常。 - 可选参数给默认值,别用
null占位(coupon默认""而不是null)。 - 校验集中在
build()(或私有构造器):items非空、金额非负、时间区间合法——一处集中,比散落在 setter 里可靠。 build()里做防御性拷贝:List.copyOf(b.items)既复制又不可变,防止调用方拿着 builder 继续改。- Builder 不要复用、不要跨线程共享:builder 自己是可变的,
build()之后继续item(...)会让"下一次构建"带上上一次的残留。
五、建造者 vs 工厂 vs 原型:三者到底差在哪
三个创建型模式都解决"怎么得到对象",但关注的焦点完全不同

| 维度 | 工厂方法 / 抽象工厂 | 建造者 | 原型 |
|---|---|---|---|
| 关注点 | 创建哪个类(一个产品族) | 怎么一步步装配 | 复制哪个已有实例 |
| 过程 | 一次调用拿到成品 | 分多步,最后 build() | 一次 copy() |
| 交付物 | 直接是可用对象 | 复杂对象的不同表示 | 原型的副本 |
| 组合关系 | 一族产品要配套 | 常与组合模式一起用(构建树) | 与注册表/缓存搭配 |
| 典型代码 | PaymentFactory.create("alipay") | Order.builder(...).item(...).build() | prototype.copy() |
一句话记忆:工厂选"造哪个",建造者管"怎么造",原型管"照着谁造"。
六、现代 Java 里的替代方案
建造者不是唯一的答案,很多场景下有更轻的写法:
| 方案 | 适用 | 代价 |
|---|---|---|
| 构造器 | 参数 ≤ 3 个、必填为主 | 参数一多就不可读 |
| 静态工厂 + 具名方法 | 需要表达"哪种构造方式",如 Order.giftOrder(id, customer) | 变体多了方法会膨胀 |
record + wither | 不可变数据载体;字段不多 | 每次"改一个字段"都要新建对象 |
| 流式建造者 | 参数 ≥ 4 个、大量可选参数、需要集中校验 | 多写一个类;builder 本身可变 |
| Kotlin / Scala 的命名参数 + 默认值 | 语言层面已解决该问题 | Java 没有 |
record + wither 的写法值得单独看一眼:
/** Java 16+:record 天然不可变,紧凑构造器负责校验,wither 负责"改一点" */
public class RecordWitherDemo {
record Point(int x, int y, String label) {
Point { // 紧凑构造器:集中校验
if (label == null) label = "";
}
Point withX(int newX) { // wither:返回新对象
return new Point(newX, y, label);
}
}
public static void main(String[] args) {
Point p = new Point(1, 2, "起点");
System.out.println(p.withX(10)); // Point[x=10, y=2, label=起点]
}
}
关于 Lombok @Builder:它能省掉手写 builder 的样板代码,但有两个坑需要知道——
@Builder生成的 builder 不做业务校验:items为空、金额为负都能构建成功;需要自己加@Builder(buildMethodName = "buildInternal")再包一层校验,或把校验放进私有构造器。@Builder.Default的行为容易踩:字段直接初始化 +@Builder时,不写@Builder.Default会让默认值在 builder 路径下失效(变成null/0)。
结论:@Builder 适合"纯数据载体",一旦对象有业务不变量,校验必须自己补。
七、选型:什么时候用哪一种

判断口诀:参数少就别用;只有一种表示就用流式建造者;要有多种表示、且构建顺序是业务规则,才上 Director。
八、什么时候不要用建造者
- 参数只有 2~3 个:
new Point(1, 2)比Point.builder().x(1).y(2).build()清楚一百倍。《Effective Java》给的门槛是 4 个参数。 - 对象本来就该是简单值:上建造者只会让调用链变长、调试变难(断点要跨好几个栈帧)。
- 真正需要的是工厂:如果你纠结的是"该 new 哪个子类",那是工厂模式的活儿。
- 真正需要的是原型:如果对象来自"复制一份现成的再微调",用原型/拷贝构造器更直接。
- 只是为了"看起来高级":给每个实体类都配 Builder,是典型的样板代码膨胀——尤其在用了
record、或字段本来就少的情况下。
九、参考资料
- GoF《设计模式:可复用面向对象软件的基础》——Builder 章(意图、角色、协作、适用性)
- Joshua Bloch《Effective Java》Item 2:遇到多个构造器参数时要考虑使用构建器(4 参数门槛、与伸缩式构造器的对比)
- 《Effective Java》Item 50:必要时做防御性拷贝(
build()里List.copyOf的原因,也对应上一篇文章的原型深拷贝话题) - JDK 中的建造者:
StringBuilder、java.net.http.HttpRequest.Builder、java.util.stream.Stream.Builder
评论