本栏是博主根据如题教材进行Java进阶时所记的笔记,包括对原著的概括、理解,教材代码的报错&运行情况。十分建议看过原著遇到费解地方再来参考或与博主讨论。致敬作者Joshua Bloch跟以杨春花为首的译者团队。
遇到多个构造器参数时要考虑用构建器
一般当我们需要多个可选参数时,会这样做:
public class BadNutritionFacts {
private int servingSize; //ml
private int servings; //per container
private int calories; //
private int fat; //g
private int sodium; //mg
private int carbohydrate; //mg
public BadNutritionFacts(int servingSize, int servings) {
this(servingSize , servings ,0);
}
public BadNutritionFacts(int servingSize, int servings, int calories) {
this(servingSize , servings , calories ,0);
}
public BadNutritionFacts(int servingSize, int servings, int calories, int fat) {
this(servingSize , servings , calories , fat , 0);
}
public BadNutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize , servings , calories , fat , sodium , 0);
}
public BadNutritionFacts(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;
}
但这样的话随着参数增加,容易失控,可读性也奇差,而且就算我们不小心颠倒了其中两个参数的顺序,也不会报错。这时推荐使用JavaBeans模式:
public class NutritionFacts {
private int servingSize = -1; //ml
private int servings = -1; //per container
private int calories = 0; //
private int fat = 0; //g
private int sodium = 0; //mg
private int carbohydrate = 0; //mg
public NutritionFacts() {
super();
}
public void setServingSize(int servingSize) {
this.servingSize = servingSize;
}
public void setServings(int servings) {
this.servings = servings;
}
public void setCalories(int calories) {
this.calories = calories;
}
public void setFat(int fat) {
this.fat = fat;
}
public void setSodium(int sodium) {
this.sodium = sodium;
}
public void setCarbohydrate(int carbohydrate) {
this.carbohydrate = carbohydrate;
}
}
为了解决{@link BadNutritionFacts}中的问题,在使用时我们调用一个无参构造器创建对象,然后用setter方法设置各个必要参数、可选参数。
public static void main(String[] args) {
NutritionFacts nutritionFacts = new NutritionFacts();
nutritionFacts.setServings(10);
nutritionFacts.setServingSize(20);
nutritionFacts.setCalories(50);
nutritionFacts.setSodium(90);
//use nutritionFacts do sth
}
但是这种方法仍然存在问题:调用setter方法过程中存在状态不一致问题,需要额外的线程安全控制,而且这种情况调试起来非常困难。于是,我们现在祭出更加优秀的一种设计方法——兼顾线程安全和可读性的Builder模式:
Builder模式编写出来一个公共的静态内部类Builder,在静态内部类中提供链式调用的方法(return this和build()),Builder设构造方法以传入不可缺省的必要参数,私有化BestNutrition的构造方法,为使用者提供Builder的构造方法来构造对象:
public class BestNutritionFacts {
//Required parameters
private int servingSize; //ml
private int servings; //per container
//Optional parameters
private int calories; //
private int fat; //g
private int sodium; //mg
private int carbohydrate; //mg
public void nutritionFactsInfo(){
System.out.println("servingSize , servings , calories , fat , sodium , carbohydrate");
System.out.println(servingSize + " , " + servings + " , " + calories + " , " +fat + " , " + sodium + " , " +carbohydrate);
}
private BestNutritionFacts(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;
}
public static class Builder{
//Required parameters
private int servingSize; //ml
private int servings; //per container
//Optional parameters
private int calories = 0; //
private int fat = 0; //g
private int sodium = 0; //mg
private int carbohydrate = 0; //mg
public Builder(int servingSize , int servings){
this.servingSize = servingSize;
this.servings = servings;
}
public Builder calories(int val){
this.calories = val;
return this;
}
public Builder fat(int val) {
this.fat = val;
return this;
}
public Builder sodium(int val) {
this.sodium = val;
return this;
}
public Builder carbonhydrate(int val) {
this.carbohydrate = val;
return this;
}
public BestNutritionFacts build(){
return new BestNutritionFacts(this);
}
}
}
另外,builder可以在builder方法中对参数强加约束条件,并在对象域而不是builder域中对它们进行检验,或者也可以直接在setter中进行检验,这样就不必等到builder才发现错误,比如修改后的Builder.build():
public BestNutritionFacts build(){
if (this.servings < 0 || this.servingSize < 0){
System.out.println("illegal args!");
throw new IllegalArgumentException();
}
return new BestNutritionFacts(this);
}
builder十分灵活,单个builder可以创建多个对象,其参数也可以在创建期间进行调整,也可以实现比如主键自增等操作。另外,builder还可以通过参数生成抽象工厂:
/**
* builder的抽象工厂,通过泛型控制返回类型,
* 而在实现该接口的类中加以限制如Tree buildTree(Builder<? extends Node> nodeBuilder);进行约束。
*
* 传统的抽象工厂是用Class对象的newInstance充当build的一部分,但newInstance总是调用其无参构造方法,
* 而不管这种构造方法存不存在,于是客户端就需要处理InstantiationException或者IllegalAccessException;
* 加上newInstance会传递无参构造方法所抛出的异常,即使newInstance缺乏相应的throws子句。
* 也就是说,newInstance破坏了Java的异常检查,
* 而使用Builder的异常检测机制则可以解决这个问题{@link nutrition.BestNutritionFacts}
*
* @author LightDance
*/
public interface Builder<T> {
public T build();
//然后创建Builder类时implements一下就可以了
}
最后是Builder模式的短板:
builder的主要缺陷是为了创建对象不得不先编写一个Builder对象,这或许会在对于性能要求十分严格的情况下造成一些问题。而且有些时候Builder方法也会非常冗长,因此只在参数很多或者感觉将来版本中参数会逐渐增多的时候使用Builder.
但是,从构造方法或者静态工厂改到Builder时,会出现很多过时的构造方法和静态工厂方法,增加更改代码逻辑的困难,因此最好一开始就使用最合适的那种方法创建对象。
简而言之,在准备写代码之前要对需求有客观可靠的评估,然后根据评估结果选择敲何种风格的代码。
全代码github地址:点我点我