Java Builder模式实战案例拆解与最佳实践
目录导读
- 为什么需要Builder模式?—— 构造器膨胀之痛
- 经典Builder模式实现(含代码案例)
- 与Lombok @Builder的对比及陷阱
- 在框架源码中的身影(MyBatis、Spring)
- 高频面试问答(含代码场景题)
- 何时该用,何时不该用
为什么需要Builder模式?—— 构造器膨胀之痛
假设你在开发一个用户实体类,字段有:用户名(必填)、邮箱(必填)、年龄(可选)、地址(可选)、昵称(可选)、手机号(可选)、头像URL(可选)、生日(可选)。

传统写法一:重叠构造器
User(String name, String email) {}
User(String name, String email, int age) {}
User(String name, String email, int age, String address) {}
// ... 组合爆炸,调用时极易传错参数顺序
传统写法二:JavaBeans模式
User user = new User();
user.setName("张三");
user.setEmail("a@b.com");
user.setAge(25);
痛点:对象状态不一致,多线程下存在可见性问题,且无法实现不可变对象。
Builder模式正是为解决“参数过多 + 参数可选性 + 不可变性”而生的经典创建型设计模式。
经典Builder模式实现(含代码案例)
我们以一个Product商品类为例,实现静态内部类Builder:
public class Product {
// 所有字段设为final,保证不可变性
private final String sku; // 必填
private final String name; // 必填
private final double price; // 必填
private final String category; // 可选
private final String description; // 可选
private final boolean onSale; // 可选,默认false
// 私有构造器,只接受Builder
private Product(Builder builder) {
this.sku = builder.sku;
this.name = builder.name;
this.price = builder.price;
this.category = builder.category;
this.description = builder.description;
this.onSale = builder.onSale;
}
// 静态内部Builder类
public static class Builder {
// 必填参数通过构造器强制
private final String sku;
private final String name;
private final double price;
// 可选参数设置默认值
private String category = "未分类";
private String description = "";
private boolean onSale = false;
public Builder(String sku, String name, double price) {
this.sku = sku;
this.name = name;
this.price = price;
}
public Builder category(String category) {
this.category = category;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder onSale(boolean onSale) {
this.onSale = onSale;
return this;
}
// build方法中做参数校验
public Product build() {
if (sku == null || sku.trim().isEmpty()) {
throw new IllegalArgumentException("SKU不能为空");
}
if (price < 0) {
throw new IllegalArgumentException("价格不能为负");
}
return new Product(this);
}
}
// 只提供getter,不提供setter
public String getSku() { return sku; }
// ... 其余getter省略
}
使用方式:
Product product = new Product.Builder("SKU001", "无线机械键盘", 399.0)
.category("数码配件")
.description("青轴,RGB背光")
.onSale(true)
.build();
代码亮点:
- 必选参数通过Builder构造器强制传递,编译期保证不遗漏
- 可选参数链式调用,面向对象风格直观
build()方法集中校验,防止脏数据- 对象完全不可变,天然线程安全
与Lombok @Builder的对比及陷阱
Lombok可用一行注解简化上述代码:
@Builder
public class Product {
private String sku;
private String name;
private double price;
private String category;
private String description;
private boolean onSale;
}
但Lombok存在三大隐蔽陷阱:
- 字段默认值被覆盖:若直接写
private boolean onSale = true;,Lombok生成的Builder会把该字段初始化为false(即Java默认值),必须使用@Builder.Default注解。 - 无法强制必填参数:所有字段都成了可选,编译期无法校验。
- 与继承冲突:父类字段无法被子类Builder正确构造,需要额外
@SuperBuilder。
生产环境建议手写核心DTO的Builder,但可以接受Lombok用于一些内部临时VO类。
在框架源码中的身影
- MyBatis:
SqlSessionFactoryBuilder、Configuration.Builder大量使用Builder模式构建复杂SqlSessionFactory。 - Spring:
BeanDefinitionBuilder用于编程式构建Bean定义,UriComponentsBuilder用于构建URI。 - JDK自身:
StringBuilder本质上就是Builder模式的一种变体(可变+非线程安全)。
这些框架选择Builder,正是因为它能隐式传递大量默认配置,而用户代码只需关注核心参数。
高频面试问答(含代码场景题)
Q1:Builder模式与工厂模式的区别? A:工厂模式侧重“创建哪一类对象”的决策(如根据类型参数返回不同子类实例),Builder侧重“一步步构建同一个对象”的细节填充,当对象有超过5个字段且有多项可选时,Builder更为合适。
Q2:Builder对象本身线程安全吗?
A:不完全安全,Builder默认是可变的,如果在方法内局部使用则安全;若作为单例共享使用,需在调用build()前同步,但构建出的目标对象(Product)是不可变的,这是Builder的核心价值。
Q3:如何让Builder支持链式继承? A:采用“递归泛型”技巧,子类Builder继承父类Builder时,将自身类型作为泛型参数传入:
public class ChildBuilder extends ParentBuilder<ChildBuilder> {
// 这样就可以在子类中直接调用父类的链式方法并返回ChildBuilder
}
Q4:写一个带校验的build()方法,如何优雅处理多个校验条件?
A:可以使用Guava的Preconditions或Java标准Objects.requireNonNull,但更优雅的是在build()中调用私有校验方法,并用IllegalStateException聚合抛出多条错误消息。
何时该用,何时不该用
推荐使用场景:
- 构造器参数超过4个,且有大量可选参数
- 需要保证对象不可变(如配置类)
- 希望用户代码可读性高,避免一长串参数
不推荐使用场景:
- 参数少于3个,且全为必填
- 对象需要频繁创建、构造开销敏感(Builder额外增加了一次内存分配)
- 字段可能随时间随意增减(维护成本高)
Builder模式不是银弹,它用“代码复杂度”换取“调用清晰度”,当你下一次面对一个连IDE提示都放不下的构造器时,Builder就是那个让你从“堆砌参数”走向“优雅组装”的答案。
如果你在阅读源码时看到new Xxx.Builder()....build()的句式,现在你已经知道背后设计者的良苦用心了,动手在你的下一个实体类中实践一次,你会爱上这种构建方式。