
先说个真实感受。工作里我维护过好几个“构造参数十几个”的业务对象每次 new 出来都像在打密码传错一个参数编译期根本发现不了等跑到线上才炸出一堆 NullPointerException。后来把对象改成建造者模式实现代码评审的时候同事都松了口气。今天这篇就把建造者模式掰开揉碎讲清楚从它解决什么问题、核心角色怎么划分到经典实现、链式写法、与工厂模式的区别再到面试题和实战里踩过的坑一次讲透。不管你是准备 Java 面试、软考还是单纯想把代码写得干净点这篇都适合你。1. 为什么需要建造者模式1.1 构造器膨胀问题先还原一个最常见的场景。假设你在开发一个订单系统订单对象一开始只有订单号、用户 ID、金额三个字段构造器写起来很干净public Order(String orderId, Long userId, BigDecimal amount) { this.orderId orderId; this.userId userId; this.amount amount; }过两个迭代需求加了收货地址、优惠券 ID、支付方式、备注、预计送达时间、是否开发票、发票抬头……字段从 3 个涨到 12 个。这时候你面临几个选择。第一种继续堆构造器重载。3 个参数的、5 个参数的、8 个参数的疯狂重载。调用方根本分不清new Order(a, b, c, d, e)里的c到底是优惠券 ID 还是支付方式可读性直接归零。第二种用 JavaBean 风格也就是无参构造加一堆 setterOrder order new Order(); order.setOrderId(123); order.setUserId(456L); order.setAmount(new BigDecimal(99.00)); order.setAddress(address); order.setCouponId(c001);JavaBean 风格解决了参数顺序问题但牺牲了不可变性。对象在创建之后可以被任意修改一个本该在构造阶段就固定下来的订单号可能被后续业务代码覆盖排查起来非常痛苦。而且你没法在 setter 阶段做“必填字段校验”因为对象是逐步拼起来的什么时候才算拼完代码里没有任何约束。第三种还见过更极端的直接 new 一个空对象字段全靠反射赋值代码跑起来跟猜谜一样。建造者模式就是冲着这两个痛点来的。1.2 建造者模式的核心价值建造者模式的核心思想是把“复杂对象的构建过程”和“对象的表示”分离开。翻译成大白话你不需要关心对象内部有多少零件、这些零件怎么拼装你只需要告诉建造者自己要什么由建造者负责一步步把对象组装好。它的三个直接好处特别明显第一参数语义清晰。orderBuilder.withCouponId(c001)一眼就知道在配什么不用再去数构造器参数位置。第二保证对象不可变。builder 构建完成后调用build()生成最终对象所有字段用 final 修饰之后没人能改线程安全性也更好。第三校验时机可控。你可以在 build() 方法里做集中校验比如订单金额必须大于 0、订单号不能为空不满足条件直接抛异常而不是让一个半成品对象流入业务层。还有一个常被忽略的价值客户端代码几乎不受对象内部结构变化影响。今天订单加了“是否加急”字段用到 builder 的调用方只需要在需要的地方加一行.withUrgent(true)其他代码不用动。如果还停留在构造器重载阶段这一步能把所有调用处都改一遍。2. 建造者模式核心角色与经典实现2.1 四个角色的职责划分GoF 对建造者模式的定义是将一个复杂对象的构建与它的表示分离使得同样的构建过程可以创建不同的表示。这里涉及四个角色我一个个说。Product产品最终要构建的复杂对象比如上文的 Order。产品类里通常只提供 getter不提供 setter字段通过构造函数传入保证不可变。Builder抽象建造者定义构建产品各个部件的抽象接口比如buildOrderId()、buildAddress()以及一个返回产品的方法build()。ConcreteBuilder具体建造者实现 Builder 接口具体负责每个部件怎么构建同时记录构建过程中的中间状态。Director指挥者控制构建的步骤和顺序。它定义一个construct()方法内部按固定流程调用 builder 的方法保证“构建过程”稳定不变。这四个角色里Director 是很多初学者最容易忽略的。实际写代码时很多人直接把 Director 省掉由客户端直接操作 builder。这种做法不叫错但要分场景如果构建步骤永远只有一套流程省略 Director 反而更简洁如果同一套原料要支持多种构建顺序或多种产品变体Director 就很有必要。2.2 经典 GoF 风格实现我用一个简化版的“电脑组装”例子来写经典实现。产品是 Computer部件有 CPU、内存、硬盘另外客户端还关心是否配备显卡、是否配显示器。直接看代码。首先是产品类public class Computer { private final String cpu; private final String memory; private final String disk; private final boolean hasGpu; private final boolean hasMonitor; Computer(String cpu, String memory, String disk, boolean hasGpu, boolean hasMonitor) { this.cpu cpu; this.memory memory; this.disk disk; this.hasGpu hasGpu; this.hasMonitor hasMonitor; } public String getCpu() { return cpu; } public String getMemory() { return memory; } public String getDisk() { return disk; } public boolean isHasGpu() { return hasGpu; } public boolean isHasMonitor() { return hasMonitor; } Override public String toString() { return Computer{ cpu cpu \ , memory memory \ , disk disk \ , hasGpu hasGpu , hasMonitor hasMonitor }; } }注意构造器我用了包级私有外部不能直接 new只能通过 builder 创建。抽象建造者public interface ComputerBuilder { ComputerBuilder withCpu(String cpu); ComputerBuilder withMemory(String memory); ComputerBuilder withDisk(String disk); ComputerBuilder withGpu(boolean hasGpu); ComputerBuilder withMonitor(boolean hasMonitor); Computer build(); }具体建造者和指挥者public class StandardComputerBuilder implements ComputerBuilder { private String cpu; private String memory; private String disk; private boolean hasGpu; private boolean hasMonitor; Override public ComputerBuilder withCpu(String cpu) { this.cpu cpu; return this; } Override public ComputerBuilder withMemory(String memory) { this.memory memory; return this; } Override public ComputerBuilder withDisk(String disk) { this.disk disk; return this; } Override public ComputerBuilder withGpu(boolean hasGpu) { this.hasGpu hasGpu; return this; } Override public ComputerBuilder withMonitor(boolean hasMonitor) { this.hasMonitor hasMonitor; return this; } Override public Computer build() { if (cpu null || memory null || disk null) { throw new IllegalStateException(cpu、memory、disk 为必填项); } return new Computer(cpu, memory, disk, hasGpu, hasMonitor); } } public class ComputerDirector { private final ComputerBuilder builder; public ComputerDirector(ComputerBuilder builder) { this.builder builder; } public Computer construct() { // 这里控制组装顺序和默认配置 return builder.withCpu(i5-12400) .withMemory(16GB) .withDisk(512GB SSD) .withGpu(false) .withMonitor(true) .build(); } }客户端使用ComputerDirector director new ComputerDirector(new StandardComputerBuilder()); Computer computer director.construct();这套实现里Computer 的构造器是包级私有的意味着除了 builder其他类没法直接 new。外部如果想自定义配置也可以不经过 Director直接把 builder 拿来用。这就是“构建过程和表示分离”的体现。2.3 经典实现与链式实现的取舍上面是标准的 GoF 风格。但说实话现在 Java 项目里更常见的是把 builder 写成产品类的静态内部类并且 setter 方法返回 this形成链式调用。我个人的经验是经典四角色适合“构建过程特别复杂、有多种组装流程”的场景比如配置引擎、报表生成器而静态内部类链式写法适合大多数业务对象因为你会发现多数业务对象的构建流程根本没有那么多“顺序”可言大家都是按参数名一个一个赋值再集中校验。链式写法的典型结构是这样public class Order { private final String orderId; private final Long userId; private final BigDecimal amount; private final String address; private final String couponId; private final boolean urgent; private Order(Builder builder) { this.orderId builder.orderId; this.userId builder.userId; this.amount builder.amount; this.address builder.address; this.couponId builder.couponId; this.urgent builder.urgent; } public static Builder builder() { return new Builder(); } public static class Builder { private String orderId; private Long userId; private BigDecimal amount; private String address; private String couponId; private boolean urgent; public Builder orderId(String orderId) { this.orderId orderId; return this; } public Builder userId(Long userId) { this.userId userId; return this; } public Builder amount(BigDecimal amount) { this.amount amount; return this; } public Builder address(String address) { this.address address; return this; } public Builder couponId(String couponId) { this.couponId couponId; return this; } public Builder urgent(boolean urgent) { this.urgent urgent; return this; } public Order build() { if (orderId null || orderId.isEmpty()) { throw new IllegalStateException(orderId 不能为空); } if (userId null) { throw new IllegalStateException(userId 不能为空); } if (amount null || amount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalStateException(amount 必须大于 0); } return new Order(this); } } }使用的时候Order order Order.builder() .orderId(20250101001) .userId(10086L) .amount(new BigDecimal(99.00)) .address(某市某区某街道) .couponId(c001) .build();这段代码里有个细节值得注意原订单是“可选的优惠券、可选的地址”客户端不必调address()build 后对应字段就是 null。这是建造者模式相对构造器重载的巨大优势——你不用为每一种可选参数组合专门写一个构造器。3. 建造者模式与工厂模式的区别3.1 一图理清两种模式的侧重点这个问题在面试里出现频率极高也经常有人搞混。我先说结论工厂模式侧重“创建哪种产品”建造者模式侧重“怎么一步步创建产品”。工厂模式的核心意图是封装创建逻辑让调用方不用关心具体实现类。比如你有一个LoggerFactory调用getLogger(file)给你返回文件日志实现调用getLogger(console)给你返回控制台日志实现调用方拿到的都是Logger接口不需要知道背后是 FileLogger 还是 ConsoleLogger。建造者模式的核心意图是即便产品是同一个实现类它的构建过程也极其复杂或者参数极多需要分步骤设置并且希望构建过程校验能集中管理。建造者 mode 并不解决“选哪个实现类”的问题它解决的是“一个对象怎么从零到完整装配起来”的问题。如果用生活类比工厂模式是餐厅点餐——“我要一份宫保鸡丁”后厨决定给你做哪道菜建造者模式是 DIY 组装——“我要一台电脑CPU 选 i5内存选 16G硬盘选 512G加显卡不加显示器”每一个部件由你指定最后拼装校验。3.2 什么时候选择哪个从实际开发角度我总结了一个选择清单。如果创建对象时主要变化点是实现类不同优先考虑工厂模式。比如接口有多个实现希望通过一个统一入口创建。如果创建对象时主要变化点是内部参数不同、组合方式不同优先考虑建造者模式。比如同一个实体类不同场景要配不同字段且可选字段很多。如果对象既有多种实现类、每个实现类又有复杂的内部结构可以考虑两种模式结合用工厂模式封装不同实现类的创建入口用建造者模式封装单个实现类的内部装配。举个实际例子我之前写过一个消息推送服务PushSender有短信、邮件、站内信三种实现这里用简单工厂但每种消息都有标题、内容、模板参数、定时发送时间、重试次数等一堆字段这时候消息对象本身用建造者模式。两个模式不冲突反而互补。4. 建造者模式在 JDK 与开源框架中的身影4.1 JDK 里的典型应用StringBuilder 与 StreamBuilderBuilder 模式在 JDK 里最出名的就是StringBuilder。你可能没想过StringBuilder的设计动机之一就是解决字符串拼接时产生大量不可变中间对象的问题。它本质上是把“字符串的构建过程”独立出来你不断append最后toString()产出最终字符串。这和建造者模式的“一步步装配最终 build”如出一辙。另一个容易忽略的是java.util.stream.Stream的Builder接口StreamString stream Stream.Stringbuilder() .add(a) .add(b) .add(c) .build();这也是典型的构建者思想。中间add一个元素就相当于装配一个部件最后build()固定成一个不可变视角的 Stream。4.2 Guava、Lombok 与 Spring 中的实践Guava 的ImmutableList、ImmutableMap系列大量使用 BuilderImmutableListString list ImmutableList.Stringbuilder() .add(a) .add(b) .build();它的设计意图很明确创建不可变集合同时避免构造器参数过长。Lombok 的Builder注解更是把建造者模式变成了 Java 开发者的日常。给实体类标上BuilderLombok 会帮你生成静态内部 builder 类和链式方法。但我要提醒一句Lombok 生成的是简化版 builder它默认不做 build 期校验。如果构建时真的有必填约束要么写自定义构造器加校验要么在业务入口处校验别把 Lombok 当成万能药。Spring 框架里的UriComponentsBuilder、MockMvcRequestBuilders也都是典型的 builder。UriComponentsBuilder.fromPath(/api/user).queryParam(id, 1).build()每一步都在装配 URI 的组成部分非常直观。看这些框架级案例你会发现它们不是“为了用设计模式而用”而是确实需要解决传入参数多、可选参数多、构建后对象不可变这些实际问题。设计模式从来不是装饰品是现实约束逼出来的解法。5. 实战纯手写 Builder 的细节与规范5.1 手写 Builder 类的步骤清单如果项目里不方便引入 Lombok或者想完全掌控 builder 行为手写 builder 的套路我基本固定为这几步。第一步产品类的构造器设为私有参数是一个 Builder 类型。目的是不让外部绕过 builder 直接创建对象确保所有对象都经过校验逻辑。第二步Builder 类和产品类同包或作为静态内部类。静态内部类能直接访问产品类的私有构造器省去很多访问权限的麻烦。第三步Builder 的每个字段与产品字段一一对应类型保持一致。setter 方法命名用字段名本身比如orderId(String orderId)不要画蛇添足写setOrderId再返回 this链式调用读起来更自然。第四步build 方法内做字段校验。必要字段缺失时抛IllegalStateException错误信息要写清楚比如“orderId 不能为空”而不是笼统的 “build failed”。第五步返回产品对象时通过产品类的私有构造器把 Builder 的字段整体拷贝进去。这里有个关键点如果产品字段里有List、Map这类可变对象要在构造器或 builder 里做防御性拷贝否则外部仍可以通过修改传入的集合来破坏对象不可变性。这个问题我在生产环境真实踩过后面会细说。5.2 防御性拷贝与不可变性陷阱很多讲 builder 的文章不会提防御性拷贝但这个坑真的会出线上事故。举个场景订单对象有个ListOrderItem字段public class Order { private final ListOrderItem items; private Order(Builder builder) { this.items builder.items; } }builder 里public Builder items(ListOrderItem items) { this.items items; return this; }看起来没问题但调用方如果这样写ListOrderItem itemList new ArrayList(); itemList.add(item1); Order order Order.builder().items(itemList).build(); itemList.add(item2);因为order.items和itemList指向同一个 ArrayList第二步itemList.add(item2)之后订单对象里的 items 也多了个元素不可变性被击穿。正确做法是在赋值时拷贝public Builder items(ListOrderItem items) { this.items items null ? new ArrayList() : new ArrayList(items); return this; }build 时再包一层Collections.unmodifiableListthis.items Collections.unmodifiableList(new ArrayList(builder.items));这样即使外部拿到order.getItems()调用 add 也会抛 UnsupportedOperationException不可变才算真正落地。5.3 Builder 中的继承问题再聊一个让很多人懵掉的点Builder 的继承。产品类有父类和子类时简单的手写 builder 很容易破功。假设有个BaseOrder和子类GiftOrder如果希望GiftOrder.builder()也能设置父类的字段需要用到泛型自继承技巧也就是常见的SelfT模式public abstract class BaseOrderBuilderT extends BaseOrderBuilderT { protected String orderId; SuppressWarnings(unchecked) public T orderId(String orderId) { this.orderId orderId; return (T) this; } public abstract BaseOrder build(); } public class GiftOrderBuilder extends BaseOrderBuilderGiftOrderBuilder { private String giftName; public GiftOrderBuilder giftName(String giftName) { this.giftName giftName; return this; } Override public GiftOrder build() { return new GiftOrder(this); } }使用GiftOrder order new GiftOrderBuilder() .orderId(001) .giftName(优惠券礼包) .build();orderId返回泛型T实际类型是GiftOrderBuilder所以链式调用不会断。这个技巧在框架源码里很常见写业务代码时如果遇到继承结构可以直接照搬。没有这个技巧的话子类 builder 一旦调用父类方法返回类型变成父类 builder后面就接不上子类的独有方法了很烦。5.4 性能与对象开销问题关于性能网上有一类说法是“建造者模式有额外对象开销不建议在高性能场景用”。这个说法对但需要分清楚影响范围。每个Order.builder()都会 new 一个 Builder 对象。在创建频率极高的场景比如每秒创建几万个订单对象这个开销确实不能忽略。但它也仅仅是多一次对象分配大多数业务系统远没有到这个量级。更重要的是Builder 模式带来的代码可维护性提升远超那点微小的 GC 压力。如果你真的在极端高性能场景比如网关转发日志、埋点上报可以考虑复用 Builder 实例每次 build 前重置字段。但这样做的代价是 Builder 不再是线程安全的必须用 ThreadLocal 或严格串行调用。我一般不建议提前做这种优化等压测数据出来再动手也不迟。先让代码正确、清晰然后用基准测试说话。6. 面试与软考高频问题速查6.1 面试官到底在考什么建造者模式几乎是 Java 面试必问的设计模式之一。面试官问“讲讲建造者模式”通常不只是想听你背四个角色他们更想看这几件事第一能不能说清楚和工厂模式的区别。这个前面已经讲透核心就是“创建什么”和“怎么创建”的差别。第二有没有实际用过。很多人一上来就背概念问他项目里哪里用了就说“看过但是没写过”。这种回答非常减分。你完全可以提 StringBuiler、Lombok Builder、或自己封装过复杂查询参数对象。第三能不能现场写一个链式 Builder。建议把第 2 节的完整例子记熟并且能讲清楚 build 方法里为什么要做校验、产品类构造器为什么要私有、集合字段为什么要拷贝。第四对不可变对象的理解。建造者模式和不可变对象经常连在一起考如果顺带说出final字段、Collections.unmodifiableList、防御性拷贝这些点面试官会眼前一亮。6.2 软考设计模式速记技巧软考比如软件设计师会考设计模式分类。建造者模式在 GoF 分类里属于创建型模式Creational Pattern。创建型模式一共五个单例、工厂方法、抽象工厂、建造者、原型。记忆口诀五花八门我自己用的是“打工抽箭圆”这种谐音梗但你得找到自己能记住的那套。关键要记住建造者的三个关键词复杂对象构建、分步组装、构建与表示分离。软考如果出选择题问你“以下哪个模式适合创建参数较多的复杂对象”基本就是建造者。如果问你“将复杂对象的构建与表示分离”是原话也能直接对应。另外软考还可能给一段 UML 类图让你识别模式。建造者的 UML 特征很鲜明Product 和 Director 都依赖 Builder 接口ConcreteBuilder 实现 BuilderProduct 不直接依赖 Director。看到这种结构答案基本锁定建造者。6.3 八股文里的几个变体问题除了基础问题分享几个我见过的变体面试题。“建造者模式为什么不用抽象类”答案是 Builder 接口往往是稳定的语义定义用接口更灵活可以让一个类同时实现多个 Builder 接口但如果你想在 Builder 基类里放公共的字段和默认实现抽象类也有优势。所以这是一个“按场景选”的问题不是非黑即白。“build() 方法和工厂方法有什么区别”build 方法处理的是同一个产品对象内部的装配工厂方法返回的是不同类型的产品如果某个 build() 方法内部根据参数判断返回不同类型那它就更像工厂方法了。“Builder 模式能否解决构造函数参数过多的问题和 JavaBean 相比有什么优势”这个问题前面已经给过答案可读性、不可变性、校验集中。“如果产品需要支持多种配置组合Builder 是不是最佳选择”分情况。如果组合是有限的几种用 Director 或枚举配置更清晰如果组合近乎无限Builder 更合适。7. 一些踩坑经验与实际感受7.1 别为了用模式而用模式在团队里看过不少过度设计的代码。一个只有两个字段的简单 VO也硬套 Builder然后每个调用处都是Xxx.builder().a(a).b(b).build()。说实话这种代码不如直接写构造器。设计模式是工具不是教条。字段不超过三个、参数顺序稳定、没有校验需求的时候构造器就是最清晰的方案。我判断一个对象是否需要 Builder 的经验法则字段数量在 4 个以上或者未来大概率增长。存在多个可选字段客户端只关心其中一部分。构建之后对象不允许被修改需要不可变保证。构建阶段需要做参数校验或字段间联动校验。满足两条以上才值得引入 Builder。只有一条可以再想想。7.2 Lombok 与手写 Builder 的实际选择团队里用 Lombok 很普遍我自己的态度是简单 DTO、VO、请求参数对象直接用Builder提高效率核心领域对象、有严格不可变性要求的建议手写 Builder 并自定义校验逻辑。Lombok 的 Builder 生成代码时有个细节很多人不知道它默认生成的是builder()静态方法和全字段构建方法如果你需要忽略某些字段或者设置默认值可以用Builder.Default。但说到底Lombok 生成 builder 的过程中是不会有校验逻辑的。团队规范里还可以通过自定义注解处理来扩展 Lombok 的行为比如在 build() 时自动调用实体的 validate 方法。这块能力很强也特别适合规范统一的团队。如果不放心就手写。7.3 现场排查案例一个因可变字段引发的线上问题最后讲一个真实案例也是我最初对防御性拷贝如此敏感的原因。之前做营销系统发奖接口创建用户礼包对象礼包里有个ListRewardItem。当时只图快Builder 的字段直接赋值没有做拷贝。结果有一个定时任务在发奖过程中往同一个rewardItemList里追加了新的奖励项因为内存里引用共享礼包对象里的奖励列表跟着变了最直观的影响是对账数据对不上用户实际收到的和记录里的不一致。排查了很久最后发现是共享集合引用搞的鬼。从那之后我给自己定了条规矩凡是 Builder 里出现集合或可变对象一律拷贝进产品对象返回时再包不可变视图。这个习惯救了我不少次。8. 最后分享一个提升 Builder 实用性的技巧如果你手写的 Builder 已经用得很顺手可以再给产品类加一个静态方法from用于把已有对象转换为 Builder方便做对象拷贝或部分字段修改。比如订单有“复制一单但金额改成新的”这种需求Order copy Order.builder() .orderId(NEW-001) .userId(original.getUserId()) .amount(newAmount) .address(original.getAddress()) .couponId(original.getCouponId()) .build();每个字段都手写一遍很啰嗦。可以先在 Order 里写一个builderFrom(Order existing)public static Builder builderFrom(Order existing) { return Order.builder() .orderId(existing.getOrderId()) .userId(existing.getUserId()) .amount(existing.getAmount()) .address(existing.getAddress()) .couponId(existing.getCouponId()) .urgent(existing.isUrgent()); }之后复制订单就变成Order copy Order.builderFrom(original) .orderId(NEW-001) .amount(newAmount) .build();减少重复代码也让复用意图一目了然。这个模式在很多框架里都有叫 “copy builder”做法虽小但日常开发里非常提效。建造者模式说到底不是什么高深理论它就是在“对象构造越来越复杂”的时候帮我们把代码变得可读、可靠、可维护的一套成熟解法。真正用好了代码评审都会轻松很多。