第059篇 建造者模式:链式调用为何无处不用在哪些地方

简介: 建造者模式核心是**将复杂对象的创建过程外置、分步可控、集中校验**。它解决参数过多、互斥依赖、分步初始化问题,关键在于:私有构造、链式setter、build()统一校验、产物不可变(final+防御拷贝)、默认值内聚于Builder——非仅为语法糖,而是工程化构造控制。

建造者模式在面试里常被问"和工厂有什么区别",但真正能区分开的是另一件事:建造者把"创建一个复杂对象的过程"从对象本身挪到了外部,并且要求这个过程分步可控、随时可校验。很多人以为它就是给构造函数拆参数,其实那只是表象;真正值钱的地方是"分步 + 校验 + 复用 + 不可变"这四件事。

先把结论放在前面:建造者模式(Builder)解决的是构造参数过多、参数组合爆炸、对象分步初始化三类问题。核心手法是把一个类拆成"不可变的构建者"与"最终产物"两半:构建者每个 setter 返回 this 支持链式调用,build() 时统一做参数校验并产出对象。 判断该不该用的标准很直接:构造函数超过 4 个参数、或者参数之间存在互斥与依赖关系、或者对象需要分多步才能填满。

机制拆解

以 Android 里的 AlertDialog.Builder 或网络请求构造器为例,看清每一步在解决什么。第一步,私有化带参构造,把构造函数藏起来,强制外部走构建者——这样就断掉了"有人直接 new 出一个参数不完整的对象"这条路径。第二步,字段在构建者与产物各存一份,构建者可改、产物只读;build() 里的 new 用构建者的当前值填充,产物此后不可变。 第三步,每个 setter 返回 this,让调用可以写成一行链式代码,可读性来自"一行只做一件事"的顺序表达。第四步,校验集中在 build(),也就是对象即将诞生的那一刻——此时所有参数都已就位,是做一致性校验的唯一正确时机。

第五步,默认值写在本类而非调用方。这一点是本篇标题的由来:默认值写在 Builder 里,是这个类自己的领域知识;写在每个调用点,就是把领域知识抄成了散落各处的重复代码。Android 里最典型的例子是 Retrofit 的 Builder、Glide 的 RequestOptions、以及各种 Dialog 参数默认值。

这些坑的正确绕法

最常见的坑是 build() 里没做必填校验,非法配置一路带到运行期深处才爆。 表现是空指针或下标越界出现在离赋值点很远的地方,堆栈看不出是哪个参数缺了。根因是构建过程被拆成多步后,校验如果分散在各 setter 里,会漏;即使不漏,也无法表达"这几个参数互斥、这几个必须同时出现"这种跨参数规则。 规矩是:必填校验、互斥校验、范围校验一律写在 build() 开头,失败要抛带明确信息的异常,异常信息里要写清是哪个参数不合法。

其次是把 Builder 建成可变又允许复用,同一个构建者产出两个"配置互相污染"的对象。 典型 bug 是先 new Builder().setA(1).build(),之后又 .setB(2).build() 一次,第二个对象里 a 也变成了 1 之外的预期值。根因是构建者自身被复用。 两种修法:要么每次 new 一个构建者(Android 里最常见),要么提供一个 copy()/不可变构建者;Java 里常见的 StringBuilder 之所以可复用,是因为它的每次 append 本身就是预期行为,而领域构建者不是。

还有一个更隐蔽的坑:给可变对象加了一个 Builder,但没有真正做不可变。 表现是 build() 之后仍能改产物的字段(集合没做防御性拷贝、字段不是 final),于是"不可变对象"名存实亡,跨线程共享时又冒出难以复现的问题。 修法是字段逐个 final、返回集合时用 Collections.unmodifiableList 或 Kotlin 的 toList() 做一次拷贝。

代码里见真章

看一段能直接跑的代码,把上面的机制落到具体写法上:

// Builder:私有构造 + 链式 setter + build() 集中校验 + 产物不可变
public final class Request {
   
    private final String url;             // 字段逐个 final,构造后不可变
    private final int timeoutMs;
    private final Map<String, String> headers;
    private final byte[] body;

    private Request(Builder b) {
            // 构造函数私有,外部只能走 Builder
        this.url = b.url;
        this.timeoutMs = b.timeoutMs;
        this.headers = Collections.unmodifiableMap(new HashMap<>(b.headers));  // 防御性拷贝
        this.body = b.body == null ? null : b.body.clone();
    }

    public static final class Builder {
   
        private String url;               // 领域默认值写在本类,不散落到调用方
        private int timeoutMs = 10_000;
        private final Map<String, String> headers = new LinkedHashMap<>();
        private byte[] body;

        public Builder url(String u) {
    this.url = u; return this; }   // 返回 this 支持链式
        public Builder timeoutMs(int t) {
   
            if (t <= 0) throw new IllegalArgumentException("timeout 必须 > 0");
            this.timeoutMs = t; return this;
        }
        public Builder header(String k, String v) {
    this.headers.put(k, v); return this; }

        public Request build() {
             // 校验集中在这里,一步到位
            if (url == null || url.isEmpty()) {
   
                throw new IllegalStateException("必填项 url 未设置");
            }
            if (url.startsWith("http://")) {
      // 互斥/约束规则只在这里能表达
                throw new IllegalStateException("仅允许 https: " + url);
            }
            if (body != null && body.length > MAX_BODY) {
   
                throw new IllegalStateException("body 超过上限 " + MAX_BODY);
            }
            return new Request(this);      // 一次性固化
        }
    }
    // 用法:顺序即语义,读起来像一句话
    Request r = new Request.Builder().url("https://api/x").header("A", "1").build();
}

这段代码值得盯三处:第一处,字段全 final + 集合防御性拷贝,让产物真正不可变;第二处,build() 里同时做必填、协议约束、体积上限三类跨参数校验——这些规则在 setter 阶段无法表达;第三处,默认值 timeoutMs = 10_000 定义在本类,调用方不用重复,也不必知道默认值是多少。

这题在面试里怎么问、怎么答

"建造者和工厂模式有什么区别?"分三层:①建造者分步、关注怎么拼;工厂一步到位,关注造哪一个;②建造者常用于同一类型有多种参数组合,工厂常用于多种类型的选择(此时建造者常被用作工厂内部实现);③建造者产出的是同一种类型,工厂产出的是接口下的不同实现。 Android 上的例子:Retrofit.Builder 是建造者(拼接口实现与配置),ListAdapter 的多类型工厂是工厂。

"建造者什么时候反而是负担?"给判据:参数只有两三个且互不相关时,建造者只是多写三倍代码;对象需要分步创建、中途可能被访问时,建造者隐藏了中间态会让人困惑("到底哪一步之后才算有效");不可变要求不强时,它带来的 final 与拷贝成本没有回报。

"和 Kotlin 的命名参数/default 参数相比呢?"Kotlin 的 data class + 默认参数 + 具名调用在很多场景下更简洁,不必写 Builder。差别在三点:Kotlin 默认参数不适用于 Java 调用方、参数超过十几个时可读性也会崩、且默认参数无法表达"参数组合互斥"这类跨参数规则。 Android 里给 Java 调用的组件库(第三方 SDK)仍要提供 Builder。

"使用中遇到过什么问题?"案例一:某个配置类允许 setA 与 setB 同时设置,但两者冲突时运行期抛错;定位到校验分散在各 setter,只查了单参数合法性;修复为把互斥规则集中到 build() 并补上组合参数的单测。案例二:一个 Builder 被复用来造两个不同配置的请求,第二个请求带上了第一个的残留值;修复为每次新建构建者,并把返回集合改为防御性拷贝。

再补一个工程上值得讲清的点:Android 上的对话框、通知、请求构造几乎都是 Builder,它们的默认值就是"领域知识的沉淀处"。 举例:如果播放器的默认缓冲策略、默认超时、默认重试次数写在 SDK 的 Builder 里,业务方只在特殊场景覆盖;当产品同学要改默认值时只改一处,且升级 SDK 后新默认值自动生效。 反之如果默认值散在十几个业务页面的构造代码里,改一次要翻十几个文件,还容易漏。这是建造者模式在工程上最实在的价值——比"链式调用好看"重要得多。

给正在准备面试的你

把建造者画成一张"分步固化"的示意图:左边一列是构建者里的可变状态(url、timeout、headers,箭头指向每一步的 setter),右边一列是产物里的 final 字段。中间画一个大箭头标 build(),箭头上写三行字——必填校验、跨参数约束、一次性固化。产物那边画一个锁形图标标"不可变",并注明"集合做防御性拷贝"。 再在图下方写一句判据:"参数 > 4、参数间有约束、需要分步填满"三条满足两条就用 Builder。面试中建造者的问题,基本都能从这张图推导出答案。

再补工程案例与踩坑——应用落点是把项目里参数超过四个的构造调用统一改走 Builder,把 build() 前的校验集中化并补上"缺必填、传互斥值、超范围"三条单测,同时检查所有返回集合的地方是否做了防御性拷贝。

复习时别孤立刷题:单例模式六种写法——单例解决唯一性、建造者解决构造过程的可控性,两者都在解决"对象的诞生方式"。

划两句重点:私有构造 + 链式 setter + build() 集中校验 + 产物不可变(final + 防御性拷贝);默认值写在 Builder 里是领域知识;构建者不可复用,产物不可变。

下一篇聊 Java 阶段面试通关地图:六十个考点串联复盘——沿着今天这条主线继续往前走。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。

「Android软件开发面试·从入门到精通」连载系列

上一篇:单例模式六种写法:线程安全与懒加载的平衡

下一篇预告:Java-阶段面试通关地图:六十个考点串联复盘

有任何问题欢迎在评论区留言交流。

相关文章
|
1天前
|
自然语言处理 API 开发者
GEO 内容工程实践:推荐答案的信息位结构与占位度评估方法
本文提出GEO内容工程中推荐答案的“信息位结构”模型,将实体提及分解为5个语义功能不同的信息位(识别、定位、品类、特征、佐证),并定义“占位度”评估其表达完整性。强调替换代价差异,提供4个可复现实验与归因诊断流程,聚焦测量机制而非产品实现。(239字)
|
13小时前
|
缓存 安全 Java
第060篇 Java 阶段面试通关地图:六十个考点串联复盘
“Java阶段面试”不是考知识点记忆,而是考察机制理解、场景落地与工程权衡。本文以五条主线(集合、并发、JVM、异常、新特性)为地图,强调“结论+机制+场景+代价”四层回答法,直击HashMap树化、线程池调优、Android-JVM差异等高频深水区,助你从背题党蜕变为真懂原理的工程师。
24 0
|
13小时前
|
存储 设计模式 安全
第056篇 新时间 API:LocalDateTime 取代 Date 的理由
移动端时间处理易出线上事故:`SimpleDateFormat` 静态共享致线程不安全;跨时区“昨天”判断偏差;字符串截取本地化日期失效。Java 8 `java.time` 核心在于厘清**三类时间语义**:`Instant`(UTC瞬时点)、`LocalDateTime`(无时区墙上时间)、`ZonedDateTime`(含时区规则,支持夏令时)。存储用 `Instant`,展示才绑定时区;格式化必传 `Locale`;`YYYY`≠`yyyy`;`Period`(日历量)与 `Duration`(物理量)不可混用。
25 0
|
15小时前
|
缓存 监控 Java
第021篇 异常体系 Throwable:Checked 与 Unchecked 的边界
Android面试高频题:Throwable异常体系,需透彻理解Error/Exception区别、受检/非受检划分逻辑,以及finally执行机制。重点在于“为什么这样设计”和“用错的后果”——如吞异常致线上脏数据、滥用异常控流程拖垮性能、finally抛异常掩盖根因等。真懂者必踩过坑、用过、复盘过。
17 0
|
16小时前
|
Java API 开发工具
第005篇 数组与多维数组:批量数据的地基
数组是Java中最基础却易被低估的定长连续容器,本质为“数组的数组”(多维),越界抛异常、拷贝为浅拷贝、`length`是字段非方法。常见坑:`Arrays.asList(基本类型数组)`返回size=1、误当深拷贝、与List混淆。工程中重性能、少扩容、严校验。
18 0
|
13小时前
|
安全 编译器 Android开发
第074篇 区间与遍历:until、downTo 与 step
Kotlin区间与遍历深度解析:`1..10` 创建`IntRange`对象(有内存开销),而`until`/`downTo`/`step`等被编译器优化为零对象计数循环;推荐`0 until size`替代`0..size-1`,避免空集合越界;`downTo`需显式`step -1`防死循环;区间仅用于索引,勿`map`/`filter`滥用。
20 0
|
13小时前
|
缓存 Java 编译器
第069篇 object 与 companion object:Kotlin 里的单例
本文深入剖析 `object` 与 `companion object` 的本质差异:二者虽均为单例,但字节码结构、初始化时机及 Java 可见性截然不同。`object` 编译为私有静态实例(饿汉式),而 `companion object` 是宿主类内的静态 `Companion` 实例,其初始化绑定类加载。`@JvmStatic` 和 `@JvmField` 不提升性能,而是改变 Java 调用形态与反射可见性。核心警示:慎用 `object` 持有可变状态,伴生对象避免耗时初始化——正确选型应基于作用域需求,而非便利性。
24 0
|
14小时前
|
安全 Java Android开发
第034篇 Iterator 与 fail-fast:并发修改异常的根源
Iterator 与 fail-fast 是 Java 集合的“防错机制”:通过 modCount 与 expectedModCount 校验,遍历中结构修改即抛 ConcurrentModificationException。它**不是并发安全保证**,仅单线程下尽早暴露错误;删除须用 it.remove(),多线程应选 CopyOnWriteArrayList 或加锁。
14 0
|
13小时前
|
SQL 缓存 Java
第039篇 synchronized 原理:对象头、锁升级与重量级锁
`synchronized` 底层依托对象头 Mark Word 与 `monitorenter` 指令,锁随竞争升级:无锁 → 偏向锁(JDK15 已废弃)→ 轻量级锁(CAS 自旋)→ 重量级锁(ObjectMonitor + 内核阻塞)。`wait` 必在同步块内调用,确保条件检查与等待原子性。
15 0
|
13小时前
|
缓存 监控 Java
第049篇 JVM 调优入门:从 OOM 日志倒推问题
JVM调优核心是方法论而非参数背诵:先分类(堆/元空间/直接内存/栈OOM)、再取证(GC日志/内存快照/线程栈)、接着定性(泄漏 or 配置不足)、最后动参。顺序错则掩盖问题,盲目调大堆只会延迟OOM。Android需额外关注堆分区、largeHeap及meminfo诊断。
20 0