← 返回题目列表

继承层次中如何设计 Builder?为什么容易写错?

高频 困难 第 11 / 26 题 更新于 2026/08/01
建造者模式继承泛型BuilderJava

简化版

继承层次中的 Builder 难点在于父类字段和子类字段都要参与构建,同时链式调用要返回正确的子类 Builder 类型。常见方案是使用自限定泛型让父类 Builder 方法返回 T extends Builder<T>,或在简单场景中避免继承,改用组合。

详细版

普通 Builder 在单个类里很好写,但遇到继承就会出现两个问题:

  • 父类字段要复用,不想每个子类 Builder 重复写;
  • 父类 Builder 方法如果返回父类类型,调用完父类方法后就拿不到子类方法。

比如 VehiclebrandmaxSpeedCar 还有 seatCount。如果 brand() 返回 Vehicle.Builder,那么 .brand("BMW").seatCount(5) 可能编译失败,因为返回类型已经退回父类 Builder。

常见解决方式是父类 Builder 使用泛型:

abstract static class Builder<T extends Builder<T>> {
    String brand;

    public T brand(String brand) {
        this.brand = brand;
        return self();
    }

    protected abstract T self();
}

子类 Builder 实现 self() 返回自己:

static class Builder extends Vehicle.Builder<Builder> {
    int seatCount;

    public Builder seatCount(int seatCount) {
        this.seatCount = seatCount;
        return this;
    }

    protected Builder self() {
        return this;
    }
}

面试里要注意补充:继承 Builder 代码复杂度较高,适合稳定继承层次;如果只是为了复用几个字段,组合、嵌套配置对象或静态工厂可能更清晰。

完整版教学

一、为什么继承会破坏普通链式 Builder

链式 Builder 的基础是每个方法都返回当前 Builder。单类场景里写 return this 很自然,但父类方法的返回类型通常是父类 Builder。问题出在静态类型上:编译器只看声明类型,不会因为运行时对象是子类 Builder 就自动开放子类方法。

Car car = Car.builder()
        .brand("BMW")
        .seatCount(5)
        .build();

如果 brand() 声明返回 Vehicle.Builder,那么 .brand("BMW") 后面的静态类型就是 Vehicle.Builder,它没有 seatCount()。这不是链式调用语法问题,而是继承和返回类型协变没有设计好。

易错点:继承 Builder 的核心问题不是字段复制,而是“父类链式方法返回后还能不能继续调用子类方法”。

二、自限定泛型的基本思路

常见写法是让父类 Builder 接收一个类型参数 T,这个 T 表示“具体子类 Builder 类型”。父类方法返回 T,并通过 self()this 转成正确类型。

abstract class Vehicle {
    protected final String brand;
    protected final int maxSpeed;

    protected Vehicle(Builder<?> builder) {
        this.brand = builder.brand;
        this.maxSpeed = builder.maxSpeed;
    }

    abstract static class Builder<T extends Builder<T>> {
        private String brand;
        private int maxSpeed = 120;

        public T brand(String brand) {
            this.brand = brand;
            return self();
        }

        public T maxSpeed(int maxSpeed) {
            this.maxSpeed = maxSpeed;
            return self();
        }

        protected abstract T self();
    }
}

T extends Builder<T> 看起来绕,本质是在告诉编译器:父类 Builder 方法返回的是“某个具体 Builder 自己”。这类写法也叫 self type 或 CRTP 风格。

三、子类 Builder 如何接上父类字段

子类 Builder 继承父类 Builder,并把自己的类型作为泛型参数传进去。这样父类的 brand()maxSpeed() 返回的就是 Car.Builder,后面可以继续调用 seatCount()

public class Car extends Vehicle {
    private final int seatCount;

    private Car(Builder builder) {
        super(builder);
        this.seatCount = builder.seatCount;
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder extends Vehicle.Builder<Builder> {
        private int seatCount = 5;

        public Builder seatCount(int seatCount) {
            this.seatCount = seatCount;
            return this;
        }

        public Car build() {
            if (seatCount <= 0) {
                throw new IllegalArgumentException("seatCount must be positive");
            }
            return new Car(this);
        }

        protected Builder self() {
            return this;
        }
    }
}

此时调用链可以保持自然:

Car car = Car.builder()
        .brand("BMW")
        .maxSpeed(240)
        .seatCount(5)
        .build();

这段代码里,父类字段由父类 Builder 管理,子类字段由子类 Builder 管理,最终构造时子类先调用 super(builder) 交给父类复制字段,再复制自己的字段。

四、用数字看继承 Builder 的复杂度

假设父类有 4 个字段,3 个子类各有 5 个字段。如果不用继承 Builder,每个子类 Builder 都要重复写父类 4 个字段的设置方法,总共重复 4 * 3 = 12 个方法。使用父类 Builder 后,这 4 个方法只写一次。

不复用:
  CarBuilder: 4 个父字段方法 + 5 个子字段方法
  TruckBuilder: 4 个父字段方法 + 5 个子字段方法
  BikeBuilder: 4 个父字段方法 + 5 个子字段方法
  父字段方法重复 12 次

泛型父 Builder:
  Vehicle.Builder: 4 个父字段方法
  每个子 Builder: 5 个子字段方法 + self() + build()

但复杂度也会上升:读者要理解泛型边界、self()、父子构造器协作。如果继承层次只有 1 个子类,或者父字段只有 1 到 2 个,可能没必要引入这套结构。

五、继承 Builder 中校验应该放在哪里

父类字段的通用校验放父类 Builder 或父类构造器兜底,子类字段的校验放子类 build()。如果校验涉及父子字段组合,例如 Vehicle.maxSpeed > 200Car.seatCount 不能超过 2,就要在子类 build() 中统一判断,因为只有子类 Builder 同时知道父类字段和子类字段。

校验内容推荐位置原因
brand 非空父类 Builder 或父类构造器所有子类共享
maxSpeed 范围父类 Builder 或父类构造器通用规则
seatCount 范围子类 Builder子类字段
maxSpeed 与 seatCount 组合子类 build()需要同时看父子字段

父类构造器最好保留关键不变量兜底,防止未来新增子类 Builder 时漏掉通用校验。Builder 负责友好的构建 API,Product 构造器负责守住对象不变量。

六、Lombok @SuperBuilder 与手写 Builder 的取舍

Lombok 提供 @SuperBuilder 支持继承层次 Builder,能减少大量模板代码。它适合字段多、继承结构稳定、团队熟悉 Lombok 的项目。代价是生成代码不直观,排查构造行为时要看反编译或 IDE 展开结果。

面试中可以这样评价:@Builder 适合单类,继承场景要用 @SuperBuilder 才能处理父子字段;但它仍不能替代业务校验。尤其是默认值、集合防御性拷贝、构造器可见性、框架反序列化兼容,都需要人工确认。

如果项目对编译插件依赖敏感,或者对象构造规则很复杂,手写 Builder 更透明。自动生成适合消除重复,但不能把设计责任也交给注解。

七、什么时候应该避免继承 Builder

继承 Builder 的存在前提是:产品类真的存在稳定的继承层次。如果只是为了复用字段,比如很多请求对象都有 requestIdtenantIdtimeoutMs,直接抽父类未必合适。组合一个 RequestMeta 可能更简单。

继承方案:
  BaseRequest <- CreateOrderRequest
  BaseRequest <- PayOrderRequest

组合方案:
  CreateOrderRequest has RequestMeta
  PayOrderRequest has RequestMeta

组合的优势是字段复用不绑定类型继承,演进更灵活。设计模式面试里,能主动说出“继承 Builder 很强,但组合经常更稳”,通常比只背泛型写法更有工程判断。

八、常见误区与追问

  • 误区:父类 Builder 方法直接返回父类 Builder 就够了。 这样链式调用会丢失子类方法,调用体验被破坏。
  • 误区:继承结构里每个子类 Builder 复制父类字段最简单。 短期简单,长期会让默认值和校验规则分散重复。
  • 误区:用了 Lombok @SuperBuilder 就不用理解原理。 注解只是生成代码,父子字段、默认值和校验边界仍要设计。
  • 追问:为什么需要 self() 方法? 父类无法直接知道具体子类 Builder 类型,self() 用来把链式返回交给子类确定。
  • 追问:父类校验放父类还是子类? 通用不变量放父类,子类特有和父子组合规则放子类 build()
  • 追问:多层继承还能继续套吗? 可以,但泛型会更复杂,超过 2 到 3 层时要重新评估继承本身是否合理。
  • 追问:组合和继承怎么选? 稳定的 is-a 关系用继承;只是字段复用或能力复用时优先组合。

九、加强记忆

继承 Builder 记住三个关键词:父类字段复用、返回类型不丢、校验边界清楚。泛型 T extends Builder<T> 是为了解决链式返回类型,self() 是为了让父类方法返回具体子类 Builder;如果只是复用几个字段,组合往往比继承更清爽。