文章目录
第二章 创建和销毁对象
第1条 用静态工厂方法代替构造器
对于类而言,为了让客户端获取它自身的一个实例,最传统的方法就是提供一个公有的构造器。还有一种方法, 也应该在每个程序员的工具箱中占有一席之地。类可以提供一个公有的静态工厂方法(static factory method),它只是返回类的实例的静态方法。下面是一个来自Boolean(基本类型boolean的装箱类)的简单示例。这个方法将boolean基本类型值转换成了一个Boolean对象引用:
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
注意,静态工厂方法与设计模式中的工厂方法模式不同。本条目中所指的静态工厂方法并不直接对应于设计模式中的工厂方法。
如果不通过公有的构造器,或者说除了共有的构造器之外,类还可以给他的客户端提供静态工厂方法。提供静态工厂方法而不是共有的构造器,这样做既有优势,也有劣势。
优势:
- 它们有名称。
如果构造起的参数本身没有确切地描述正被返回的对象,那么具有适当名称的静态工厂会更容易使用,产生的客户端代码也更易于阅读。例如:构造器BigInteger(int, random)返回的BigInteger可能为素数,如果用名为BigInteger.probablePrime的静态工厂方法来表示,显然更为清楚。
一个类只能有一个带有指定签名的构造器。编程人员通常知道如何避开这一限制:通过提供两个构造器,他们的参数列表只在参数类型的顺序上有所不同。实际上这并不是个好主意。面对这样的API,用户永远也记不住该用哪个构造器,结果尝尝会调用错误的构造器。并且在读到使用了这些构造器的代码时,如果没有参考的文档,往往不知所云。
由于静态工厂方法有名称,所以它们不受上述限制。当一个类带有多个相同签名的构造器时,就用静态工厂方法代替构造器,并且仔细的选择名称以便突出静态工厂方法之间的区别。 - 不必再每次调用它们的时候都创建一个新对象。
这使得不可变类可以使用预先构建好的实例,或者将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。Boolean.valueOf(boolean)方法说明了这项技术,它从来不创建对象。这种方法类似于享元模式。如果程序经常请求创建相同的对象,并且创建对象的代价很高,则这项技术可以极大地提升性能。
静态工厂方法能够为重复的调用返回相同的对象,这样有助于类总能严格控制在某个时刻哪些实例应该存在。这种类被称作实例受控的类(instance-controlled)。编写实例受控的类,有几个原因。实例受控使得类可以确保它是一个Singleton或者是不可实例化的。它还使得不可变的值类可以确保不会存在两个相等的实例,即当且仅当a==b时,a.equals(b)才为true。这是享元模式的基础。枚举类型保证了这一点。 - 他们可以返回原返回类型的任何子类型的对象。
这样我们在选择返回对象的类时就有了更大的灵活性。这种灵活性的一种应用是,API可以返回对象,同时又不会使对象的类变成公有的。以这种方式隐藏实现类会使API变得非常简洁。这项技术适用于基于接口的框架,因为在这种框架中,接口为静态工厂方法提供了自然返回类型。
在Java 8 之前,接口不能有静态方法,因为按照惯例,接口Type的静态工厂方法被放在一个名为Types的不可实例化的半生类中。例如:Java Collections Framework的集合接口有45个工具实现,分别提供了不可修改的集合,同步集合等等。几乎所有这些实现静态工厂方法在一个不可实例化的类中导出。所有返回对象的类都是非公有的。
现在的Collection Framework API比导出45个独立公有类的那种实现方式要小得多,每种便利实现都对应一个类。这不仅仅是指API数量上的减少,也是概念意义上的减少;为了使用这个API,用户必须掌握的概念在数量和难度上都减少了。程序员知道,被返回的对象是由相关的接口精确指定的,所以他们不需要阅读有关的类文档。此外,使用这种静态工厂方法时,甚至要求客户端通过接口来引用被返回的对象,而不是通过它的实现类来引用被返回的对象,这是一种良好的习惯。 - 所返回的对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值。
只要是已声明的返回类型的子类型,都是允许的。返回对象的类也可能随着发型版本的不同而不同。
EnumSet没有公有的构造器,只有静态工厂方法。在OpenJDK实现中,它们返回两种子类之一的一个实例,具体取决于底层枚举类型的大小:如果它的元素有64个或者更少,就像大多数枚举类型一样,静态工厂方法就会返回一个RegalarEnumSet实例,用单个long进行支持;如果枚举类型有65个或者更多元素,工厂会返回JumboEnumSet实例,用一个long数组进行支持。
这两个实现类的存在对于客户端来说是不可见的。如果RegalarEnumSet不能再给小的枚举类型提供性能优势,就可能从未来的发行版本中将它删除,不会造成任何负面的影响。同样地,如果证明对性能有好处,也可能在未来的发行版本中添加第三甚至第四个EnumSet实现。客户端永远不知道也不关心它们从工厂方法中得到的对象的类,它们只关心它是EnumSet的某个子类。 - 方法返回的对象所属的类,在编写包含该静态工厂方法的类时可以不存在。
这种灵活的静态工厂方法构成了服务提供者框架(Service Provider Framework)的基础,例如JDBC API。服务提供者框架是指这样一个系统:多个服务提供者实现一个服务,系统为服务提供者的客户端提供多个实现,并把它们从多个实现中解耦出来。
服务提供者中有三个重要的组件:服务接口(Service Interface),这是提供者实现的;提供者注册API(Provider Registeration API),这是提供者用来注册实现的;服务访问API(Service access API),这是客户端用来获取服务的实例。服务访问API是客户端用来指定某种选择实现的条件。如果没有这样的规定,API就会返回默认实现的一个实例,或者允许客户端遍历所有可用的实现。服务访问API是“灵活的静态工厂”,它构成了服务提供者框架的基础。
服务提供者框架的第四个组件服务提供者接口(Service Provider Interface)是可选的,它表示产生服务接口之实例的工厂对象。如果没有服务提供者接口,实现就通过反射方式进行实例化。对于JDBC来说,Connection就是其服务接口的一部分,DriverManager.registerDriver是提供者注册API,DriverManager.getConnection是服务访问API,Driver是服务提供者接口。
服务提供者框架模式有着无数种变体。例如,服务访问API可以返回比提供者需要的更丰富的服务接口。这就是桥接(Bridge)模式。依赖注入框架可以被看作是一个强大的服务提供者。从Java 6开始,Java平台就提供了一个通用的服务提供者框架java.util.ServiceLoader,因此你不需要(一般来说也不应该)再自己编写了。JDBC不用ServiceLoader,因为前者比后者出现的早。
劣势:
- 类如果不含公有的或者受保护的构造器,就不能被子类化。
例如,要想将Collections Framework中的任何便利的实现子类化,这是不可能的。但是这样也许会因祸得福,因为它使用程序员使用复合(composition),而不是继承,这正是不可变类所需要的。 - 程序员很难发现它们。
在API文档中,他们没有像构造器那样在API文档中明确标识出来,因此,对于提供了静态工厂方法而不是构造起的类来说,要想查明如何实例化一个类是非常苦难的。javadoc工具总有一天会注意到静态工厂方法。同时,通过在类或者接口注解中关注静态工厂,并遵守标准的命名习惯,也可以弥补这一劣势。下面是静态工厂方法的一些惯用名称。这里只列出了其中的一小部分:- from — 类型转换方法,它只有当个参数,返回该类型的一个相对应的实例,例如:
Date d = Date.from(instant);
- of —聚合方法,带有多个参数,返回该类型的一个实例,把它们合并起来,例如:
Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
- valueOf — 比from和of更繁琐的一种替代方法,例如:
BigInteger prime = BigInteger.valueOf(Interger.MAX_VALUE);
- instance或者getInstance — 返回的实例是通过方法的参数来描述的,但是不能说与参数具有同样的值,例如:
StackWalker luke = StackWalker.getInstance(options);
- carate 或 newInstance — 像instance 或者 getInstance一样,但create或者newInstance能够确保每次调用都返回一个新的实例,例如:
Ojbect newArray = Array.newStance(classObject, arrayLen);
- getType — 像getInstance一样,但是在工厂方法处于不同的类中的时候使用。type 表示工厂方法返回的对象类型,例如:
FileStore fs = Files.getFileStore(path);
- newType — 像newInstance一样,但是在工厂方法处于不同的类中的时候使用。type 表示工厂方法所返回的对象类型,例如:
BufferReader br = Files.newBufferReader(path);
- type — getType 和 newType 的简版,例如:
List< Complaint> litany = Collections.list(legcayLitany);
- from — 类型转换方法,它只有当个参数,返回该类型的一个相对应的实例,例如:
简而言之,静态工厂方法和公有构造器都各有用处,我们需要理解他们各自的长处。静态工厂经常更加合适,因此禁忌第一反应就是提供公有的构造器,而不先考虑静态工厂。
第2条 遇到多个构造器参数时要考虑构建器
静态工厂和构造器有个共同的局限性:它们都不能很好地扩展到大量的可选参数。比如用一个类表示包装食品外面显示的营养成分标签。这些标签中有几个域是必需的:每份的含量、每罐的含量以及每份的卡路里。还有超过20个的可选域:总脂肪量、饱和脂肪量、转换脂肪、胆固醇、钠、等等。大多数产品在某几个可选域中都会有非零的值。
对于这样的类,应该用哪些构造器或者静态工厂来编写呢?程序员一惯采用层叠构造器模式,在这种模式下,提供的第一个构造器只有必要的参数,第二个构造器有一个可选参数,第三个构造器有两个可选参数,以此类推,最后一个构造器包含所有可选的参数。下面有个示例,为了简单起见,它只显示4个可选域:
// 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;
}
}
当你想要创建实例的时候,就利用参数列表最短的构造器,但该列表中包含了要设置的所有参数:
NutritionFacts cocaCola = new NutritionFacts(240, 8, 100, 0, 35, 27);
这个构造器调用通常需要许多你本不想设置的参数,但还是不得不为它们传递值。在这个例子中,我们给 fat 传递了一个值为0。如果“仅仅”是这个6个参数,看起来还不算太糟糕,问题是随着参数数组的增加,它很快就失去了控制。
简而言之,重叠构造器模式可行,但是当有许多参数的时候,客户端代码就会很难编写,并且仍然难以阅读。 如果读者想知道那些值是什么意思,必须很仔细的数着这些参数来探个究竟。一长串类型相同的参数会导致一些微妙的错误。如果客户端不小心颠倒了两个参数的顺序,编译器也不会出错,但是程序在运行时会出现错误的行为。(详见第51条)。
遇到许多可选的参数构造起的时候,还有第二种替代方法,即 JavaBean 模式,在这种模式下,先调用一个无参构造器来创建对象,然后再调用setter方法来设置每个必要的参数,以及每个相关的可选参数:
// 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 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) {
<