建造者模式

一个有 20 个参数的构造函数

候选人小张在字节面试时,面试官翻到他的代码,问:"这个 User 类有多少个字段?"

小张说:"20 个。"

面试官:"构造函数有几个参数?"

小张:"...20 个。"

面试官沉默了三秒,说:"这 20 个参数,面试官怎么知道哪个是哪个?你自己三个月后还记得吗?"

小张的构造函数是这样的:

public class User {
    private String firstName;
    private String lastName;
    private int age;
    private String phone;
    private String email;
    private String address;
    private String city;
    private String zipCode;
    private boolean emailVerified;
    private boolean phoneVerified;
    // ... 还有 10 个字段

    public User(String firstName, String lastName, int age,
                String phone, String email, String address,
                String city, String zipCode, boolean emailVerified,
                boolean phoneVerified /* ... 10 more */) {
        // 20 个参数,调用时根本不知道谁是谁
    }
}

这就是建造者模式要解决的问题:让对象创建既可读灵活


一、核心思想

建造者模式把一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

直接 new:     new User("John", "Doe", 30, "123456", "john@email.com", ...)
建造者模式:   User.builder()
                   .firstName("John")
                   .lastName("Doe")
                   .age(30)
                   .phone("123456")
                   .email("john@email.com")
                   .build()

二、手写建造者🔴

2.1 标准写法

public class User {
    // 不可变字段
    private final String firstName;
    private final String lastName;
    private final int age;
    private final String phone;
    private final String email;

    // 可选字段
    private final String address;
    private final String city;
    private final boolean emailVerified;

    // 私有构造函数,只能通过 Builder 创建
    private User(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.phone = builder.phone;
        this.email = builder.email;
        this.address = builder.address;
        this.city = builder.city;
        this.emailVerified = builder.emailVerified;
    }

    // 静态工厂方法,入口
    public static Builder builder() {
        return new Builder();
    }

    // 内部 Builder 类
    public static class Builder {
        // 必需参数
        private final String firstName;
        private final String lastName;
        private final int age;

        // 可选参数,使用默认值
        private String phone = "";
        private String email = "";
        private String address = "";
        private String city = "";
        private boolean emailVerified = false;

        public Builder(String firstName, String lastName, int age) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.age = age;
        }

        public Builder phone(String phone) {
            this.phone = phone;
            return this; // 链式调用关键
        }

        public Builder email(String email) {
            this.email = email;
            return this;
        }

        public Builder address(String address) {
            this.address = address;
            return this;
        }

        public Builder city(String city) {
            this.city = city;
            return this;
        }

        public Builder emailVerified(boolean verified) {
            this.emailVerified = verified;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

2.2 使用方式

// 清晰、类型安全、支持 IDE 自动补全
User user = User.builder()
    .firstName("John")
    .lastName("Doe")
    .age(30)
    .phone("1234567890")
    .email("john@example.com")
    .city("Beijing")
    .build();

对比构造函数调用:

// 根本不知道每个参数是什么意思
new User("John", "Doe", 30, "1234567890", "john@example.com",
         "", "Beijing", false);
// 如果参数顺序记错了?bug 就埋下了

2.3 为什么构造函数是私有的?

// ❌ 如果构造函数是 public
User u1 = new User("John", "Doe", 30); // 直接绕过了 Builder
User u2 = new User("John", "Doe", 30); // 不走 Builder,默认值失效

// ✅ 构造函数 private,强制使用 Builder
User u = User.builder().firstName("John").lastName("Doe").age(30).build();

强制使用 Builder 的好处:

  1. 所有对象创建都经过同一个入口
  2. 可以统一加验证逻辑(在 build() 方法中)
  3. 可以在 Builder 中加默认值处理

【架构权衡】 构造函数私有化 + Builder 是构建不可变对象的最佳组合。不可变对象天然线程安全,没有同步开销,没有意外修改风险。但代价是每个类都要写一个对应的 Builder,代码量翻倍。


三、Lombok @Builder 偷懒利器🟡

3.1 一行注解搞定

import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class User {
    private final String firstName;
    private final String lastName;
    private final int age;
    private final String phone;
    private final String email;
    private final String address;
    private final String city;
    private final boolean emailVerified;
}

Lombok 自动生成:

  • UserBuilder 内部类
  • User.builder() 静态工厂方法
  • build() 方法
  • 所有 withXxx() 链式调用方法

3.2 Lombok 的坑

@Builder
public class User {
    private final String firstName;  // final 字段必须在构造函数中初始化
}

Lombok 生成的 Builder 会把所有字段都设为可选(使用默认值)。但如果你想让某些字段必须设置,需要配合 @Builder.Default 或手动处理:

@Builder
public class User {
    @Builder.Default
    private String city = "Beijing";  // 有默认值的可选字段

    private final String firstName;   // 没有默认值 = 必需
}
⚠️

Lombok 的 @Builder 会生成一个无参构造函数(如果类中没有显式的构造函数)。但如果你手动加了一个有参构造函数,Lombok 就不会生成无参构造函数了。解决方法是用 @Builder 配合 @AllArgsConstructor@NoArgsConstructor


四、建造者模式的进阶用法🟡

4.1 参数校验

public User build() {
    // 建造者模式最适合在这里做校验
    if (firstName == null || firstName.isBlank()) {
        throw new IllegalArgumentException("firstName is required");
    }
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("age must be between 0 and 150");
    }
    if (email != null && !email.contains("@")) {
        throw new IllegalArgumentException("invalid email format");
    }

    return new User(this);
}

4.2 默认值与必填校验分离

public static class Builder {
    // 必填字段在构造函数中
    public Builder(String firstName, String lastName) {
        if (firstName == null) throw new IllegalArgumentException("firstName required");
        if (lastName == null) throw new IllegalArgumentException("lastName required");
        this.firstName = firstName;
        this.lastName = lastName;
    }

    // 可选字段使用链式 setter,默认值在字段声明处设置
    private int age = 0;           // 默认值
    private String city = "Beijing"; // 默认值
    private boolean verified = false;

    // 链式方法
    public Builder age(int age) {
        this.age = age;
        return this;
    }

    public Builder city(String city) {
        this.city = city;
        return this;
    }

    public Builder verified(boolean verified) {
        this.verified = verified;
        return this;
    }

    public User build() {
        if (age > 150) throw new IllegalArgumentException("age too large");
        return new User(this);
    }
}

4.3 建造者 + 继承

如果你的类需要被继承,Builder 也要继承:

// 基类
public abstract class Person<T extends Person<T>> {
    protected final String name;
    protected final int age;

    protected Person(Builder<?, ?> builder) {
        this.name = builder.name;
        this.age = builder.age;
    }

    public static abstract class Builder<T extends Builder<T>> {
        protected String name = "";
        protected int age = 0;

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

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

        protected abstract T self();
        public abstract Person build();
    }
}

// 子类
public class Employee extends Person<Employee> {
    private final String department;

    private Employee(EmployeeBuilder builder) {
        super(builder);
        this.department = builder.department;
    }

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

    public static class EmployeeBuilder
            extends Person.Builder<EmployeeBuilder> {

        private String department = "";

        public EmployeeBuilder department(String department) {
            this.department = department;
            return this;
        }

        @Override
        protected EmployeeBuilder self() {
            return this;
        }

        @Override
        public Employee build() {
            return new Employee(this);
        }
    }
}

这个模式叫 "沿链传递"(Simulated Self-Type),是 Effective Java Item 2 推荐的写法。


五、建造者 vs 其他创建模式

维度构造函数工厂方法建造者
参数数量多(难以阅读)少-中多(链式可读)
可选参数需要多个构造函数需要多个工厂一个 Builder 搞定
不可变性需额外努力需额外努力配合私有构造函数天然支持
代码量多(手写时)
适用场景简单对象相关对象族复杂对象

【面试官心理】 我问他建造者模式,最想看的是他有没有理解"为什么需要这个模式"。能说出"解决构造函数参数过多问题"的只是基本操作,能说出"建造者模式配合不可变对象"的才是真正理解的设计思路。如果他还能提到 Lombok @Builder,那是用过工具链的加分项。


六、生产避坑清单

6.1 可变 Builder 的陷阱

// ❌ 危险:Builder 创建后可以被修改
User.UserBuilder builder = User.builder().firstName("John").lastName("Doe");
User user1 = builder.age(30).build();
User user2 = builder.age(40).build(); // user1 的 age 也被改了!

// ✅ 安全:build() 后返回的 User 是不可变的
// Builder 本身的属性在 build() 后仍然可变,但这是个隐患
// 更好的做法是在 build() 后清空 Builder 状态,或让 Builder 本身不可变

6.2 建造者的性能

// ❌ 错误:每次构建都 new 一个 Builder
public User createUser(String name, int age) {
    UserBuilder builder = new UserBuilder(); // 每次都 new
    return builder.name(name).age(age).build();
}

// ✅ 改进:如果频繁创建相似的对象,考虑复用 Builder
private static final User.UserBuilder BASE_BUILDER = User.builder()
    .firstName("Default")
    .lastName("User");

public User createUser(String name, int age) {
    return BASE_BUILDER
        .clone() // 需要自己实现 clone
        .name(name)
        .age(age)
        .build();
}

6.3 什么时候不用建造者

// ❌ 过度使用:只有两个参数的类不需要 Builder
public class Point {
    private final int x;
    private final int y;

    // Lombok 自动生成:
    // - 全参构造函数
    // - builder()
    // - builder().x().y().build()
    // 杀鸡焉用牛刀
}
@Builder
public class Point {
    private final int x;
    private final int y;
}

// ✅ 简单对象直接用构造函数
Point p = new Point(1, 2);
// 更好的方式:Lombok @AllArgsConstructor 后
Point p = Point.builder().x(1).y(2).build();

:::tip 💡 经验法则:参数少于等于 4 个时,构造函数足够清晰。超过 4 个可选参数时,建造者模式开始体现价值。但超过 10 个参数?不用建造者就是在给自己挖坑。


七、面试总结

7.1 核心问题

  1. "建造者模式和构造函数比,有什么优势?" —— 必问。参数可读性、默认值处理、不可变性支持。
  2. "你用过 Lombok @Builder 吗?有没有踩过坑?" —— 实际用过的人会有感悟。
  3. "什么场景下你会选择建造者模式?" —— 核心是"参数过多 + 部分参数可选 + 需要不可变对象"。
  4. "建造者模式能解决什么问题?" —— 构造函数参数过多难以维护、参数顺序记不住、无法表达语义。

7.2 级别差异

级别期望回答
P5能写出标准建造者结构
P6理解不可变对象 + 建造者的组合,知道 Lombok @Builder
P7能说清楚与工厂模式的区别,能在复杂继承体系中正确使用建造者