effective java
2. 遇到多个构造器参数时要考虑使用构建器
静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。 比如 用一个类表示包装食品外面显示的营养成分标签。 这些标签中有几个域是必需的:每份的含 量、每罐的含量以及每份的卡路里。 还有超过 20 个的可选域: 总脂肪量、饱和脂肪量、转 化脂肪、胆固醇、纳,等等。 大多数产品在某几个可选域中都会有非零的值。
对于这样的类,应该用哪种构造器或者静态工厂来编写呢?程序员一向习惯采用重叠构造器( telescoping constructor)模式,在这种模式下,提供的第一个构造器只有必要的参 数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,依此类推,最后一个构造器包含所有可选的参数。下面有个示例,为了简单起见,它只显示四个可选域:
//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) optiona1
private final int fat; // (g/serving) optional
Private final int sodium; // (mg/serving) optiona1
private final int carbohydrate; // (g/serving) optiona1
public NutritionFacts (int servingSize , int servings) {
this (servingSize, servings, 0);
}
public NutrftionFacts(int servingSize, int servings, int calories) {
this(servingSize , servings, calories, 0);
}
public NutritionFacts(int servingSize, int servings, int calortes, int fat) {
this(servingSize, servings, calortes, fat , 0);
}
public NutritionFacts(int servingSize, int servings, int calorfes, 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;
}
}
当你想要创建实例的时候,就利用参数列表最短的构造器,但该列表中包含了要设置 的所有参数:
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35 , 27);
这个构造器调用通常需要许多你本不想设置的参数,但还是不得不为它们传递值。 在 这个例子中,我们给 fat 传递了一个值为 0。 如果“仅仅”是这 6 个参数,看起来还不算太 糟糕,问题是随着参数数目的增加,它很快就失去了控制。
简而言之,重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难缩写, 并且仍然较难以阅读。 如果读者想知道那些值是什么意思,必须很仔细地数着这些参数来探 个究竟。 一长串类型相同的参数会导致一些微妙的错误。 如果客户端不小心颠倒了其中两个 参数的顺序,编译器也不会出错,但是程序在运行时会出现错误的行为(详见第 51 条)。
遇到许多可选的构造器参数的时候,还有第二种代替办法,即 JavaBeans 模式,在这 种模式下,先调用一个无参构造器来创建对象,然后再调用 setter 方法来设置每个必要的参数,以及每个相关的可选参数:
// JavaBeans Pattern - allows inconsistency, mandates mutability
public class NutrftionFacts {
// Parameters initialized to default values (if any]
Private int servingSize = -1; // Required; no defau1t va1ue
private int servings = -1; // Required; no default value
private int calories = 0;
Private int fat = 0;
Prtvate 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 = va1; }
}
这种模式弥补了重叠构造器模式的不足。 说得明白一点,就是创建实例很容易,这样 产生的代码读起来也很容易:
NutrftionFacts cocaCola = new NutrftionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setcarbohydrate(27);
遗憾的是, JavaBeans 模式自身有着很严重的缺点。 因为构造过程被分到了几个调用中, 在构造过程中 JavaBean 可能处于不一致的状态。 类无法仅仅通过检验构造器参数的有效性 来保证一致性。 试图使用处于不一致状态的对象将会导致失败,这种失败与包含错误的代码 大相径庭,因此调试起来十分困难。 与此相关的另一点不足在于, JavaBeans 模式使得把 类做成不可变的可能性不复存在 (详见第 17 条),这就需要程序员付出额外的努力来确保它 的线程安全。
当对象的构造完成,并且不允许在冻结之前使用时,通过手工“冻结”对象可以弥补这 些不足,但是这种方式十分笨拙,在实践中很少使用。 此外,它甚至会在运行时导致错误, 因为编译器无法确保程序员会在使用之前先调用对象上的 freeze 方法进行冻结。
幸运的是,还有第三种替代方法,它既能保证像重叠构造器模式那样的安全性,也能 保证像 JavaBeans 模式那么好的可读性。 这就是建造者( Builder)模式 [ Gamma95 ] 的一 种形式。 它不直接生成想要的对象,而是让客户端利用所有必要的参数调用构造器(或者静 态工厂),得到一个 builder 对象。 然后客户端在 builder 对象上调用类似于 setter 的方法,来设置每个相关的可选参数。 最后 客户端调用无参的 build 方法来生成通常是不可变的对象。 这个buiider通常是它构建的类的静态成员类(详见第 24 条)。 下面就是它的示例 :
// Builder Pattern
public class NutritionFacts {
Private final int servingSize;
Private final int servings;
Private final int calortes;
Private final int fat;
Private final int sodium;
private final int carbohydrate;
public static class Builder{
// Required parametes
private final int servingSize;
Prtvate final int sevings;
// Optional parametes - initialized to default values
Private int calories = 0;
Prtvate int fat = 0;
Private int sodium = 0;
Private int carbohydrate = 0;
//builer构造器,保证必传的两个参数
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 NutrtionFacts(Builder builder){
servingSize = builder.servingSize;
servings = builder.servings;
calories = bui1der.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
注意 NutritionFacts 是不可变的,所有的默认参数值都单独放在)个地方。builder 的设值方法返回 builder 本身,以便把调用链接起来,得到一个流式的 API。 下面就是其客 户端代码 :
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).calortes(100).sodium(35).carbohydrate(27). bui1d();
这样的客户端代码很容易编写,更为重要的是易于阅读。 BuiIder模式模拟了具名的可选参数,就像 Python 和 Scala 编程语言中的一样。
为了简洁起见,示例中省略了有效性检查。 要想尽快侦测到无效的参数,可以在 builder 的构造器和方法中检查参数的有效性。 查看不可变量,包括build方法调用的构造器中的多个参数。 为了确保这些不变量免受攻击, 从builder复制完参数之后,要检查对象域(详见第 50 条) 。 如果检查失败,就抛出 illegalArgumentException (详见第 72 条),其中的详细信息会说明哪些参数是无效的(详见第 75 条)。
Builder 模式也适用于类层次结构。 使用平行层次结构的 builder 时,各自嵌套在相应的 类中。抽象类有抽象的 builder,具体类有具体的 builder。假设用类层次根部的一个抽象类 表示各式各样的比萨:
// Builder pattern for class hierarchies
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(); // 详见50条
}
}
注意,Pizza.Builder 的类型是泛型(generic type),带有一个递归类型参数(recursive type parameter),详见第 30 条。 它和抽象的 self 方法一样,允许在子类中适当地进行方法链接,不需要转换类型。 这个针对 Java 缺乏 self类型的解决方案,被称作模拟的 self 类 型(simulated self-type)。
这里有两个具体的 Pizza 子类,其中一个表示经典纽约风味的比萨,另一个表示馅料内置的半月型( calzone)比萨。 前者需要一个尺寸参数,后者则要你指定酱汁应该内置还是外置:
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;
}
}
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() {
saucelnside = 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;
}
}
注意,每个子类的构建器中的 build 方法,都声明返回正确的子类: NyPizza.Builder 的 build 方法返回 NyPizza ,而 Calzone.Builder 中的则返回 Calzone。 在该 方法中,子类方法声明返回超级类中声明的返回类型的子类型,这被称作协变返回类型 (covariant return type)。 它允许客户端无须转换类型就能使用这些构建器。
这些“层次化构建器” 的客户端代码本质上与简单的 NutritionFacts 构建器一样。 为了简洁起见,下列客户端代码示例假设是在枚举常量上静态导人:
NyPizza pizza= new NyPizza.Builder(SMALL).addTopping(SAUSAGE).addTopping(ONION).build();
Calzone calzone =new Calzone.Builder().addTopping(HAM).sauceInside().build();
与构造器相比, builder 的微略优势在于,它可 以有多个可变( varargs) 参数。 因为 builder 是利用单独的方法来设置每一个参数。 此外,构造器还可以将多次调用某一个方法 而传人的参数集中到一个域中,如前面的调用了两次 addTopping 方法的代码所示。
Builder 模式十分灵活,可以利用单个 builder 构建多个对象。 buildr的参数可以在调用 build 方法来创建对象期间进行调整,也可以随着不同的对象而改变。 builder 可以自动填充某些域,例如每次创建对象时自动增加序列号。
Builder 模式的确也有它自身的不足。 为了创建对象,必须先创建它的构建器。 虽然创 建这个构建器的开销在实践中可能不那么明显 但是在某些十分注重性能的情况下,可能就 成问题了。 Builder 模式还比重叠构造器模式更加冗长,因此它只在有很多参数的时候才使 用,比如 4 个或者更多个参数。 但是记住,将来你可能需要添加参数。 如果一开始就使用构 造器或者静态工厂,等到类需要多个参数时才添加构造器,就会无法控制,那些过时的构造器或者静态工厂显得十分不协调。 因此,通常最好一开始就使用构建器。
简而言之, 如果类的构造器或者静态工厂中具有多个参数,设计这种类时, Builder模式就是一种不错的选择, 特别是当大多数参数都是可选或者类型相同的时候。 与使用 重叠构造器模式相比,使用 Builder 模式的客户端代码将更易于阅读和编写,构建器也比 JavaBeans 更加安全。
3.用私有构造器或者枚举类型强化Singleton属性
Singleton 是指仅仅被实例化一次的类 [ Gamma95 ]。 Singleton 通常被用来代表一个无状态的对象,如函数(详见第 24 条),或者那些本质上唯一的系统组件。 使类成为 Singleton 会使它的客户端测试变得十分困难,因为不可能给 Singleton 替换模拟实现,除非实现一个充当其类型的接口 。
实现 Singleton 有两种常见的方法。 这两种方法都要保持构造器为私有的,并导出公有的静态成员,以便允许客户端能够访问该类的唯一实例。 在第一种方法中,公有静态成员是 个 final 域:
// Singleton with public final field
public class Elvis {
public static final Elvis INSTANCE= new Elvis();
Private Elvis() { .. . }
public void leaveTheBuilding() { ... }
}
私有构造器仅被调用一次,用来实例化公有的静态 final 域 Elvis . INSTANCE。 由于 缺少公有的或者受保护的构造器,所以保证了 Elvis 的全局唯一性 : 一旦 Elvis 类被实例 化,将只会存在一个 Elvis 实例 ,不多也不少。 客户端的任何行为都不会改变这一点,但 要提醒一点:享有特权的客户端可以借助 AccessibleObject.setAccessible 方法, 通过反射机制(详见第 65 条)调用私有构造器。 如果需要抵御这种攻击,可以修改构造器, 让它在被要求创建第二个实例的时候抛出异常。
修改构造器为:
public class Elvis { public static final Elvis INSTANCE= new Elvis(); Private Elvis() { if(INSTANCE!=NULL){ throw new RuntimeException("已经创建过了"); } ... } public void leaveTheBuilding() { ... } }
在实现 Singleton 的第二种方法中,公有的成员是个静态工厂方法:
// Singleton with static factory
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
prvate Elvis() { ... }
public static Elvis getInstance() {return INSTANCE; }
public void leaveTheBuilding() { ... }
}
对于静态方法 Elvis . getinstance 的所有调用,都会返回同一个对象引用,所以,永远不会创建其他的 Elvis 实例(上述提醒依然适用)。
公有域方法的主要优势在于, API 很清楚地表明了这个类是一个 Singleton : 公有的静态域是 final 的,所以该域总是包含相同的对象引用。 第二个优势在于它更简单。
静态工厂方法的优势之一在于,它提供了灵活性: 在不改变其 API 的前提下 , 我们可 以改变该类是否应该为 Singleton 的想法。 工厂方法返回该类的唯一实例,但是,它很容易 被修改, 比如改成为每个调用该方法的线程返回一个唯一的实例。 第二个优势是, 如果应用 程序需要,可以编写一个泛型 Singleton 工厂 ( generic singleton factory) (详见第 30 条)。 使 用静态工厂的最后一个优势是,可以通过方法引用( method reference)作为提供者,比如 Elvis : : instance 就是一个 Supplier<Elvis>。 除非满足以上任意一种优势 , 否则还 是优先考虑公有域(public-field)的方法。
为了将利用上述方法实现的 Singleton 类变成是可序列化的(Serializable)(详见第 12 章),仅仅在声明中加上 implements Serializable 是不够的。为了维护并保证 Singleton, 必须声明所有实例域都是瞬时( transient )的,并提供一个 readResolve 方法(详见 第四条)。否则 ,每次反序列化一个序列化的实例时,都会创建一个新的实例,比如,在我 们的例子中,会导致“假冒的Elvis ” 。 为了防止发生这种情况,要在 Elvis 类中加入如 下 readResolve 方法:
// readResolve method to preserve singleton property
private Object readResolve () {
// Return the one true Elvis and let the garbage coll ector
// take care of the Elvis impersonatar.
return INSTANCE;
}
实现 Singleton 的第三种方法是声明一个包含单个元素的枚举类型:
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { .. . }
}
这种方法在功能上与公有域方法相似,但更加简洁,无偿地提供了序列化机制,绝对 防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。 虽然这种方法还没有广 泛采用,但是单元素的枚举类型经常成为实现 Singleton 的最佳方法。 注意,如果 Singleton 必须扩展一个超类,而不是扩展 Enum 的时候,则不宜使用这个方法(虽然可以声明枚举去实现接口)。
这一点的很多内容,跟单例模式相关,深入学习单例模式后,对这一点能更好的理解。
文档信息
- 本文作者:chayedankase
- 本文链接:https://chayedankase.github.io/2020/02/08/effective-java2/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)