当构造函数只有一两个参数的时候,一切都很顺利,但超过三个参数以后还使用构造函数来初始化就有些弊端,尤其是参数类型相似的时候,比如下面这样
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;
}
你需要仔细的核对传入的参数,,即使IDE有强大的提示功能,有时不小心将传入的参数顺序搞乱,它在编译的时候不会出错,而在运行时的反常让人摸不着头脑。也许你会尝试使用下面这个JavaBeans Pattern解决问题
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
这种分步式的调用可能导致对象的构造只完成了部分,很明显这样的对象是危险的,而且能使用set方法赋值,这个类就不能是不可变的,在多线程安全方面又处于劣势。我们推荐的方式是Builder 模式。
//不可变类,成员变量都是final
public class NutritionFacts {
private final int servings;
private final int servingSize;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
//私有的构造方法,使用build来完成初始化
private NutritionFacts(Builder builder) {
servings = builder.servings;
servingSize = builder.servingSize;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
//功能上看就是一个参数收集器,final修饰必选参数,其他为可选参数,但是要有默认值
public static class Builder {
private final int servings;
private final int servingSize;
private int calories = 0;
private int fat = 0;
private int sodium = 0;
private int carbohydrate = 0;
public Builder(int servings, int servingSize) {
this.servings = servings;
this.servingSize = servingSize;
}
//良好的函数名,调用时识别度高,不容易出错
public Builder calories(int val) {
calories = val;
return this;
}
public Builder fat(int val) {
fat = val;
return this;
}
public Builder sodium(int val) {
sodium = val;
return this;
}
public Builder carbohydrate(int val) {
carbohydrate = val;
return this;
}
public NutritionFacts build() {
return new NutritionFacts(this);
}
}
}
NutritionFacts对象的初始化既简单又安全还方便,Builder构造方法和可选的链式调用完成参数的收集,最后集中在build方法中完成NutritionFacts的初始化
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
.calories(100).sodium(35).carbohydrate(27).build();
尽量在Builder的构造函数和赋值方法中检测参数的有效性,同时,在使用Builder中的参数初始化对象字段时最好使用防御性赋值,然后再检查一番,上面的都是基本变量,看不出来。什么是防御性复制,如下示例:
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
//恶意攻击
end.setYear(78);
没有使用防御性复制,即使开始检查通过,外部仍持用引用 破坏不可变性 埋下隐患
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(start + " after " + end);
this.start = start;
this.end = end;
}
使用防御性复制,和外部断开联系 保证不变性
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(this.start + " after " + this.end);
}
下面是抽象泛型Builder,可以很好的整合在类的结构中
public abstract class Pizza {
浇头的枚举值,有火腿、蘑菇、葱、胡椒、香肠
enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
private final EnumSet<Topping> toppings;
//泛型参数必须是此Builder的子类,递归类型参数
static abstract class Builder<T extends Builder<T>> {
//使用静态工厂方法根据参数可以返回不同的子类
// EnumSet.noneOf在枚举数量小于等于64时返回的是RegularEnumSet,
//正好用一个64位的long类型通过位运算来标记添加的元素
private EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T add(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
//返回子类,这样子类不用cast也能链式调用
protected abstract T self();
abstract Pizza build();
}
public Pizza(Builder<?> builder) {
//防御性复制,防止外部持有引用破坏不变性
this.toppings = builder.toppings.clone();
}
}
子类在结构上和父类一致
public class NyPizza extends Pizza {
public enum Size {SMALL, MEDIUM, LARGE}
private final Size size;
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
//不要忘了Pissa.Builder的泛型为此Builder,否则self方法返回的是父类引用,
public static class Builder extends Pizza.Builder<Builder> {
private final Size size;
public Builder(Size size) {
this.size = size;
}
@Override
//此Builder为返回类型的子类,协变返回类型
protected Builder self() {
return this;
}
@Override
public NyPizza build() {
return new NyPizza(this);
}
}
}
public class Calzone extends Pizza {
private final boolean sauceInside;
private Calzone(Builder builder) {
super(builder);
sauceInside = builder.sauceInside;
}
public static class Builder extends Pizza.Builder<Builder> {
private boolean sauceInside = false;
@Override
protected Builder self() {
return this;
}
public Builder sauceInside() {
this.sauceInside = true;
return this;
}
@Override
public Calzone build() {
return new Calzone(this);
}
}
}
对象的构造兼具灵活和安全,可读性强不容易出错
NyPizza pizza = new NyPizza.Builder(NyPizza.Size.LARGE).add(Pizza.Topping.HAM).build();
Calzone calzone=new Calzone.Builder().sauceInside().add(Pizza.Topping.MUSHROOM).build();
这个方法的缺点在于每次创建对象都需要一个额外的Builder对象来收集参数,如果对这一点不敏感,使用Builder来创建对象是一个不错的选择,而且有些库如Lombok可以使用注解的方式帮你生成Builder类避免了繁杂的手动赋值。
网友评论