yiaz 读书笔记,翻译于 effective java 3th 英文版,可能有些地方有错误。欢迎指正。java
静态工厂方法和构造器都有一个限制:当有许多参数的时候,它们不能很好的扩展。程序员
好比试想下以下场景:考虑使用一个类表示食品包装袋上的养分成分标签。这些标签只有几个是必须的——每份的含量、每罐的含量、每份的卡路里,除了这几个必选的,还有超过 20
个可选的标签——总脂肪量、饱和脂肪量等等。对于这些可选的标签,大部分产品通常都只有几个标签的有值,不是每个标签都用到。安全
(telescoping constructor
)重叠构造器模式ide
对于这种状况,你应该选择哪一种构造器或者静态工厂方法。通常程序员的习惯是采用 (telescoping constructor
)重叠构造器模式。在这种模式中,提供一个包含必选参数的构造器,再提供其余一些列包含可选参数的构造器,第一个包含一个能够参数、第二个包含两个可选参数,以此类推下去,直到包含全部的可选参数。性能
示例代码:ui
// Telescoping constructor pattern - does not scale well! 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; } }
当你想建立一个实例的时候,你只须要找包含你须要的而且是最短参数列表的构造器便可。this
这里有一些问题,好比看下面的代码:翻译
NutritionFacts cocaCola = new NutritionFacts(240, 8, 0, 0, 35, 27);
设计
其中,第 1,2 个可选参数,咱们是不须要的,可是程序中没有提供直接赋值第 3,4个可选参数的构造器,所以,咱们只能选择包含了 1,2,3,4 个参数的构造器。这里面要求了许多你不想设置的参数,可是你又被迫的设置它们,在这里,传入对应的属性的默认值 0。而且这种模式,随着参数的增长,将变得愈来愈难以忍受,不管是编写程序的人,仍是调用程序的人。code
总而言之,(telescoping constructor
)重叠构造器模式,可使用,可是它对客户端来讲,很不友好,写和读都是一件困难的事情。它们很难搞懂那些参数对应的究竟是什么属性,必须好好的比对构造器代码。而且当参数不少的时候,很容易出 bug
,若是使用的时候,无心间颠倒了两个参数的位置,编译器是不会出现警告的,由于这里的类型同样,都是 int
,直到运行的时候才会暴露出。
Javabeans 模式
咱们还有一种选择,使用 Javabeans 模式 。
在此模式中,咱们提供一个 无参构造器 建立实例,而后利用 setXXX
方法,设置每个必须的属性和每个须要的可选属性。
示例代码:
// JavaBeans Pattern - allows inconsistency, mandates mutability 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; } }
Javabeans
模式,没有 重叠构造器模式 的缺点,对于冗长的参数,使用它建立对象,会很容易,同时读起来也是容易。正以下面看到的,咱们能够清晰的看到,每个属性的值。
NutritionFacts cocaCola = new NutritionFacts(); cocaCola.setServingSize(240); cocaCola.setServings(8); cocaCola.setCalories(100); cocaCola.setSodium(35); cocaCola.setCarbohydrate(27);
不幸运的是,Javabeans模式 自己有着严重的缺点:由于,建立对象被分割为多个步骤,先是利用无参构造器建立对象,而后再依次设置属性。这致使一个问题: Javabean 在其建立过程当中,可能处于不一致1的状态。 类不能经过检查构造器的参数,来保证对象的一致性。
另一个缺点是,将建立一个可变的类的难度提升了好几个级别,由于有 setXXX
方法的存在。
能够经过一些手段来减小不一致的问题,经过一些手段 冻结 对象,在对象被建立完成以前。而且不容许使用该对象,直到 解冻 。可是这种方式很是笨拙,在实践中不多使用。由于,编译器没法确认程序员在使用一个对象以前,该对象是否已经 解冻 。
Builder
模式
幸运的是,这里还有一种方法 Builder
模式,兼顾 重叠构造器 的安全以及 Javabean模式 的可读性。
客户端先经过调用构造器或者静态工厂方法,传入必须的参数,得到一个 builder
对象,代替直接获取目标对象。而后客户端在该 builder
对象上调用 setXXX
方法,为每个感兴趣的可选属性赋值,最后客户端调用一个 无参构造器 生成最终的目标对象,该对象通常是不可变的。其中 Builder
类是目标类的静态内部类
示例代码:
// Builder Pattern 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; } }
其中 NutritionFacts
类为不可变类,类的成员变量所有被 final
修饰,参数的默认值被放在一个地方。Builder
类 setXXX
方法返回 Builder
自己,这种写法,能够将设置变成一个链,一直点下去(fluent APIs
):
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100) .sodium(35) .carbohydrate(27) .build();
这样的客户端代码,容易编写,更容易阅读。
示例代码中,为了简洁,省去了有效性的检查。通常,为了尽快的检查到非法参数,咱们在 builder
的构造器和方法中,对其参数进行检查。
还须要检查 build
方法中调用的构造器的多个不可变参数2。此次检查延迟到 object
中,为了确保这些不可变参数不受到攻击,在 builder
将属性复制到 object
中的时候,再作一次检查。若是检验失败,则抛出 IllegalArgumentException
异常,异常信息中提示哪些参数不合法。
Bulider
模式很适合类的层次结构。可使用一个 builder
的平行结构,即每个 builder
嵌套在一个对应的类中,抽象类中有抽象的 builder
,具体类中有具体的 builder
。像下面的代码所示:
// Builder pattern for class hierarchies 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 } } 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; } } 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; } }
注意,这里的 Pizza.Builder
是类属性,被 static
修饰的,而且泛型参数,是一个 递归 的泛型参数,继承自己。和返回自身的抽象方法 self
,搭配一块儿,能够链式的调用下去,不须要进行类型的转换,这样作的缘由是,java
不直接支持 自类型 3,能够模拟自类型 4。
若是不使用模拟自类型的话,调用 addTopping
方法,返回的其实就是抽象类中的 Builder
,这样就致使没法调用子类扩展方法,没法使用 fluent APIS
。其中 build
方法,使用了 1.5
添加的 协变类型 ,它能够不用 cast
转换,就直接使用具体的类型,不然子类接收父类,是须要强转的 。
builder
模式另一个小优势:builder
能够有多个 可变参数,由于,能够将多个可变参数,放到各自对应的方法中5。另外 build
能够将多个参数合并到一个字段上,就如上面代码中 addTopping
的那样6。
builder
模式是很是灵活的。一个单一的 builder
屡次调用,能够建立出不一样的对象7。builder
的参数,能够在调用 build
方法的时候进行细微调整,以便修改建立出的对象8。builder
模式还能够自动的填充 object
域的字段在建立对象的时候。好比为每一个新建立的对象设置编号,只须要在 builder
中维护一个类变量便可。
builder
模式也是有缺点的。为了建立一个对象,咱们首先须要建立它的 builder
对象。虽然,建立 builder
对象的开销,在实践中不是很明显,可是在对性能要求很严格的场景下,这种开销能会成为一个问题。同时,builder
模式是很是冗杂的,对于比 重叠构造器 ,因此,builder
模式应该仅仅被用在构造器参数足够多的状况下,好比三个、四个或者更多,只有这样,使用 builder
模式才是值得的。可是,你要时刻记住,类在未来可能会添加新的参数,若是你一开始使用了构造器或者静态工厂方法,随着类的变化,类的属性参数变得足够多,这时候你再切换到 builder
模式,那么一开始的构造器和静态工厂方法就会被废弃,这些废弃的方法看起来很凸出,你还不能删除它们,须要保存兼容性。所以,通常一开始就选择 builder
模式是一个不错的选择。
总结,builder
模式是一个好的选择,当设计一个类的时候,该类的构造器参数或者静态工厂参数不止几个参数,尤为是许多参数是可选的或者同一个类型(可变参数)。这样设计的类,客户端代码,与静态工厂方法和重叠构造器比起来更加容易阅读和编写,和 Javabeans
模式比起来更加安全。
不一致的意思:正常对象的建立应该是一个完整的过程,这个过程控制在构造器中,能够看作是一个 原子性 的操做。它在对象建立出来之后,对象的各项属性已经被正确的初始化。可是 Javabean 模式,天生的背弃了这个原则,它的建立对象,不是一个 原子性 的操做,在构造器执行完毕之后,还有一些列的属性赋值,在这期间任何引用该对象的地方,都将得到一个不正确的对象,直到对象建立完毕。能够参考下 JavaBean disadvantage - inconsistent during construction 这里还提到了重复错误对象的建立。↩
我理解为构造器所在类的不可变属性,在 builder
中的检查相似于前台页面字段的合法性检查,最后后台(Object
)都要再次检查一遍。↩
自类型 。在支持自类型的语言中,this
或者 self
的语义,谁调用该方法,则 this
表明谁。可是在 java
中,方法中的 this
指代的是定义该方法的类型,与调用无关,致使没法很好的使用 fluent API
。可参考 java 的 self 类型。你能够验证下,打印控制台看,类型确实是调用它的类型,可是你等号左边用这个类型去接收,会提示你发现父类型,不能赋值给子类型,不知道 java
在这里面作了什么。↩
模拟自类型,这里在抽象类中,使用泛型指定,避免使用指定的类型,致使 this
被绑定为具体的。↩
构造器在建立对象的时候,构造器和普通方法同样,只能接受一个 可变参数 。可是 builder
模式,能够屡次调用不一样的方法,添加 可变参数,直到全部的可变参数所有添加完毕,再 build
建立对象。↩
一样的,构造器没法作的缘由是,构造器一经调用,对象就会被建立,也就是建立对象的过程当中,只能够调用一次构造器。可是 builder
模式能够屡次调用方法,设置参数,直到最后所有添加完毕,调用 build
建立对象。↩
仍是由于 builder
模式,只有在调用 build
方法,对象才会被建立,在建立以前,能够在调用 builder
模式的方法,修改参数,建立出不一样的对象。 能够参考下 StackOverflow
的回答: A single builder can be used repeatedly to build multiple objects↩
仍是由于 builder
模式,只有在调用 build
方法,对象才会被建立,在建立以前,能够在调用 builder
模式的方法,修改参数,建立出不一样的对象。 能够参考下 StackOverflow
的回答: A single builder can be used repeatedly to build multiple objects↩