Effective Java笔记:遇到多个构造器参数时要考虑使用构建器
在《Effective Java》的Item 2:遇到多个构造器参数时要考虑使用构建器(Builder 模式)中,当多个可选参数或复杂对象的构造过程导致构造器变得笨拙和难以管理时,使用Builder 模式来替代传统的构造器或静态工厂方法。
问题背景
1. 传统构造器参数增多的问题:
传统的构造器和静态工厂方法在处理多个参数时,会引发一些问题,包括但不限于:
- 可选参数:有些参数是必需的,有些是可选的。当参数增多时,构造器可能显得难懂。
- 重载歧义:当有多个重载构造器时,可能很难判断每个构造器的作用。
- 灵活性不足:某些情况下,部分参数暂时可能是未知或不适用,直接通过构造器设置这些参数时很麻烦。
2. 示例:使用多参数构造器的问题
假设我们设计一个用来表示食品配置信息的类 NutritionFacts,具有以下属性:
- Serving size(每份大小,必填)
- Servings(总份数,必填)
- Calories(热量,可选)
- Fat(脂肪含量,可选)
- Sodium(钠含量,可选)
- Carbohydrate(碳水化合物含量,可选)
- 传统多参数构造器的问题案例:
public class NutritionFacts {
private final int servingSize; // 必填
private final int servings; // 必填
private final int calories; // 可选
private final int fat; // 可选
private final int sodium; // 可选
private final int carbohydrate;// 可选
// 多参数构造器
public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
// 使用方式
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
问题:
- 参数管理困难:调用者必须传递所有参数,即便某些字段是默认值或无关紧要的,也必须传入。
- 比如,
100是 calorie,0是 fat,但这两个值的位置很难看出来。
- 比如,
- 可读性差:从代码中,很难马上区分每个参数的含义(特别是当某些数字值重复时,容易搞错)。
- 扩展性差:新增参数时需要引入新的构造器或修改现有构造器,导致构造器数量爆炸。
解决方案:使用 Builder 模式
Builder 模式 是一个专门用于弥补多参数构造器弊端的设计模式。通过引入一个 “构建器类(Builder)”,可以灵活地构造复杂对象,同时具有更好的可读性和易用性。
Builder 模式的实现
1. 基本实现
以下是将上述 NutritionFacts 类改用 Builder 模式的实现:
public class NutritionFacts {
// 所有字段仍然是 final 类型
private final int servingSize; // 必填
private final int servings; // 必填
private final int calories; // 可选
private final int fat; // 可选
private final int sodium; // 可选
private final int carbohydrate; // 可选
// 构造器只接受 Builder
private NutritionFacts(Builder builder) {
this.servingSize = builder.servingSize;
this.servings = builder.servings;
this.calories = builder.calories;
this.fat = builder.fat;
this.sodium = builder.sodium;
this.carbohydrate = builder.carbohydrate;
}
// Builder 静态内部类
public static class Builder {
// 必需参数
private final int servingSize;
private final int servings;
// 可选参数,初始化为默认值
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
// 构造 Builder,传入必填参数
public Builder(int servingSize, int servings) {
this.servingSize = servingSize;
this.servings = servings;
}
// 设置可选参数,返回 Builder 本身(链式调用)
public Builder calories(int calories) {
this.calories = calories;
return this;
}
public Builder fat(int fat) {
this.fat = fat;
return this;
}
public Builder sodium(int sodium) {
this.sodium = sodium;
return this;
}
public Builder carbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
return this;
}
// 返回最终构建的 NutritionFacts 实例
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
}
2. 使用 Builder 模式创建对象:
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) // 必填参数
.calories(100)
.sodium(35)
.carbohydrate(27)
.build(); // 调用 build() 创建最终对象
优点:
- 链式调用:灵活选择参数的传递顺序,代码更简洁,且更符合直觉。
- 可读性高:看到
.calories(100)就知道这是指热量,.sodium(35)就知道是钠含量。 - 扩展性强:可以轻松在 Builder 添加新参数,而无需修改已有代码逻辑。
Builder 模式的优点
-
灵活的参数组合:
- 不再需要为每种参数组合创建多个重载构造器。
- 必选参数和可选参数分离,必选参数通过 Builder 构造器传递,可选参数通过方法链设置。
-
代码可读性、可维护性更高:
- 参数有明确的上下文意义,使用
.calories()、.fat()等方法明显高于构造器的参数位置。
- 参数有明确的上下文意义,使用
-
线程安全:
- 如果
Builder不暴露到多线程环境,只在单线程中构建对象,那么构建过程是线程安全的。
- 如果
-
易扩展:
- 添加新参数而无需破坏原有 API 和调用方式,维持向后兼容。例如新增
protein参数时,仅需在Builder增加一个protein()方法和相关字段,已有类不受影响。
- 添加新参数而无需破坏原有 API 和调用方式,维持向后兼容。例如新增
实际应用中的场景和案例
Builder 模式被广泛应用于以下场景:
-
具有多个可选参数的类:
- 示例:HTTP 请求类(如请求方法、头信息、参数等可选)。
- 例:
HttpRequest.Builder在 Java 的HttpClient中体现了这种模式。
-
复杂的对象构造:
- 适合复杂对象的分步骤构造(如 SQL 查询、配置类)。
- 例:
StringBuilder的链式调用其实是一种简化版的 Builder 模式。
-
不可变对象:
- 使用 Builder 模式实现的类通常是不可变的,对象一旦构建完成,其状态就无法更改。
- 例:
java.time中的LocalDate,LocalTime。
扩展:如果参数非常多怎么办?
- 分组构建器:
如果参数很多,可以分组创建多个 Builder。
比如,Car可分为EngineBuilder和InteriorBuilder,分别负责引擎和内饰的设置。
Car car = new Car.Builder(new EngineBuilder("V8").horsepower(400).build())
.interior(new InteriorBuilder("Leather").color("Black").build())
.build();
405

被折叠的 条评论
为什么被折叠?



