2 遇到多个构造器参数时要考虑用构建器
主要内容:讲述在一个类中如果存在着大量的参数(必选参数和可选参数),有3个方法来创建对象:重叠构造器方法/set方法/Builder方法,应该选择Builder模式来创建对象。
主要目标:在针对大多数(4个或以上)参数是可选或者是相同类型的情况下,应该优先考虑使用Builder模式
实际应用场景:4个参数或者有许多可选参数的类
Effective Java知识点摘录+白话说明
-
简而言之,重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难缩写, 并且仍然较难以阅读。
当一个类中出项多个类型相同的参数时,在使用重叠构造器方法创建对象的时候容易出现这几个类型相同的参数位置错乱的情况,以Student为例,当需要创建对象时使用
public Student(String name, int iD, int age)
容易出现id
和age
调换的情况,即使编译器不会出错,但是在运行时会可能出现逻辑等的错误。//重叠构造器方法 public class NutritionFacts { private final int servingSize; // (mL) required private final int servings; // (per container) required private final int calories; // (per serving) optional private final int fat; // (g/serving) optional private final int sodium; // (mg/serving) optional private final int carbohydrate; // (g/serving) optional public NutritionFacts(int servingSize, int servings) { this(servingSize, servings, 0); } public NutritionFacts(int servingSize, int servings, int calories) { this(servingSize, servings, calories, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat) { this(servingSize, servings, calories, fat, 0); } public NutritionFacts(int servingSize, int servings, int calories, int fat, int sodium) { this(servingSize, servings, calories, fat, sodium, 0); } 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; } public static void main(String[] args) { NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27); } }
-
第二种代替办法,即 JavaBeans 模式,在这种模式下,先调用一个无参构造器来创建对象,然后再调用 sette 方法来设置每个必要的参 数,以及每个相关的可选参数。但是在构造过程中 Java Bean 可能处于不一致的状态。类无法仅仅通过检验构造器参数的有效性来保证一致性。试图使用处于不一致状态的对象将会导致失败,这种失败与包含错误的代码大相径庭,因此调试起来十分困难。与此相关的另一点不足在于, JavaBeans 模式使得把类做成不可变的可能性不复存在
使用JavaBeans模式,确实可以避免第一种模式种参数定义、设置出错的情况,但是由于
set
方法在代码的其他地方也可以使用,这就导致了在创建的过程中,对象可能处于不一致的状态,即某个时刻,对象中一个参数的值是value1,而在另外一个时刻,值是value2(value1!=value2).这个状态可能会导致在使用上出现一些错误,比如多线程中使用会出现数据不一致。虽然存在这个问题,但是可以通过冻结的操作来保证一致性。//JavaBeans方法 public class NutritionFacts { // Parameters initialized to default values (if any) private int servingSize = -1; // Required; no default value private int servings = -1; // Required; no default value private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public NutritionFacts() { } // Setters public void setServingSize(int val) { servingSize = val; } public void setServings(int val) { servings = val; } public void setCalories(int val) { calories = val; } public void setFat(int val) { fat = val; } public void setSodium(int val) { sodium = val; } public void setCarbohydrate(int val) { carbohydrate = val; } public static void main(String[] args) { NutritionFacts cocaCola = new NutritionFacts(); cocaCola.setServingSize(240); cocaCola.setServings(8); cocaCola.setCalories(100); cocaCola.setSodium(35); cocaCola.setCarbohydrate(27); } }
-
第三种替代方法,它既能保证像重叠构造器模式那样的安全性,也能 保证像 Ja vaB ean 模式那么好的可读性。这就是建造者(Builder)模式的一种形式。它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静态工厂),得到一个 builder 对象 然后客户端在 build er 对象上调用类似于 setter 的方法,来设置每个相关的可选参数。最后,客户端调用无参 build 方法来生成通常是不可变的对象。
// Builder方法 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 static class Builder { // Required parameters private final int servingSize; private final int servings; // Optional parameters - initialized to default values private int calories = 0; private int fat = 0; private int sodium = 0; private int carbohydrate = 0; public Builder(int servingSize, int servings) { this.servingSize = servingSize; this.servings = servings; } 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); } } private NutritionFacts(Builder builder) { servingSize = builder.servingSize; servings = builder.servings; calories = builder.calories; fat = builder.fat; sodium = builder.sodium; carbohydrate = builder.carbohydrate; } }
Builder模式模拟了具名的可选参数。
//创建时可以看到设置特定参数的形式 public static void main(String[] args) { NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100).sodium(35).carbohydrate(27).build(); }
Builder 模式也适用于类层次结构。
意思是:builder模式最牛逼!它可以像重叠构造器那样实现安全与一致性,也可以像JavaBeans那样实现单独设置参数,最后还可以返回一个不可变对象。它在设置参数时像setXXX方法那样易读;也可以使用类的层次结构,通过继承来让子类也实现Builder模式和让父类/子类获得一个子类对象
//父类 public abstract class Pizza { public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE } final Set<Topping> toppings; abstract static class Builder<T extends Builder<T>> { EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class); public T addTopping(Topping topping) { toppings.add(Objects.requireNonNull(topping)); return self(); } abstract Pizza build(); // Subclasses must override this method to return "this" protected abstract T self(); } Pizza(Builder<?> builder) { toppings = builder.toppings.clone(); // See Item 50 } } //子类1 public class NyPizza extends Pizza { public enum Size { SMALL, MEDIUM, LARGE } private final Size size; public static class Builder extends Pizza.Builder<Builder> { private final Size size; public Builder(Size size) { this.size = Objects.requireNonNull(size); } @Override public NyPizza build() { return new NyPizza(this); } @Override protected Builder self() { return this; } } private NyPizza(Builder builder) { super(builder); size = builder.size; } @Override public String toString() { return "New York Pizza with " + toppings; } } //子类2 public class Calzone extends Pizza { private final boolean sauceInside; public static class Builder extends Pizza.Builder<Builder> { private boolean sauceInside = false; // Default public Builder sauceInside() { sauceInside = true; return this; } @Override public Calzone build() { return new Calzone(this); } @Override protected Builder self() { return this; } } private Calzone(Builder builder) { super(builder); sauceInside = builder.sauceInside; } @Override public String toString() { return String.format("Calzone with %s and sauce on the %s", toppings, sauceInside ? "inside" : "outside"); } } //如何使用 public class PizzaTest { public static void main(String[] args) { NyPizza pizza = new NyPizza.Builder(SMALL) .addTopping(SAUSAGE).addTopping(ONION).build(); Calzone calzone = new Calzone.Builder() .addTopping(HAM).sauceInside().build(); System.out.println(pizza); System.out.println(calzone); } }
-
Builder 模式的确有它自身的不足。 为了创建对象 ,必须先创建它的构建器。虽然创建这个构建器的开销在实践中可能不那么明显 ,但是在某些十分注重性能的情况下,可能就成问题了 。
内置构造器可能会导致占用缓存过多最终影响性能。
-
Builder 模式还比重叠构造器模式更加冗长 ,因此它只在有很多参数的时候才使用,比如4个或者更多个参数。
这个模式会导致出现很多的代码,所以最好在明确了这个类里面有很多的参数才考虑使用。否则还是使用重叠构造器/set方法好。
个人总结
Q:为什么需要用到self()方法?
A:这是为了让类的内部中,在某些时候可以适当地进行方法链接,不需要转换类型。即”我不知道什么时候会用到这个内部Builder对象,所以我还是先保留着这个方法来获取Builder对象,至于以后会不会用,怎么用,以后再说啦。毕竟真用到了,我是需要确定他的类型就是builder,而不是其他类型“