Java Builder模式案例

wen java案例 1

Java Builder模式实战案例拆解与最佳实践

目录导读

  1. 为什么需要Builder模式?—— 构造器膨胀之痛
  2. 经典Builder模式实现(含代码案例)
  3. 与Lombok @Builder的对比及陷阱
  4. 在框架源码中的身影(MyBatis、Spring)
  5. 高频面试问答(含代码场景题)
  6. 何时该用,何时不该用

为什么需要Builder模式?—— 构造器膨胀之痛

假设你在开发一个用户实体类,字段有:用户名(必填)、邮箱(必填)、年龄(可选)、地址(可选)、昵称(可选)、手机号(可选)、头像URL(可选)、生日(可选)。

Java Builder模式案例

传统写法一:重叠构造器

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存在三大隐蔽陷阱:

  1. 字段默认值被覆盖:若直接写private boolean onSale = true;,Lombok生成的Builder会把该字段初始化为false(即Java默认值),必须使用@Builder.Default注解。
  2. 无法强制必填参数:所有字段都成了可选,编译期无法校验。
  3. 与继承冲突:父类字段无法被子类Builder正确构造,需要额外@SuperBuilder

生产环境建议手写核心DTO的Builder,但可以接受Lombok用于一些内部临时VO类。


在框架源码中的身影

  • MyBatisSqlSessionFactoryBuilderConfiguration.Builder大量使用Builder模式构建复杂SqlSessionFactory。
  • SpringBeanDefinitionBuilder用于编程式构建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()的句式,现在你已经知道背后设计者的良苦用心了,动手在你的下一个实体类中实践一次,你会爱上这种构建方式。

抱歉,评论功能暂时关闭!