高效 Java






4.50/5 (4投票s)
本章讨论对象的创建和销毁:何时以及如何创建它们,何时以及如何避免创建它们,如何确保它们被及时销毁,以及如何管理任何必须在其销毁前执行的清理操作。
![]() |
本章摘自《Effective Java》第二版,作者Josh Bloch,由Addison-Wesley Professional出版,ISBN 0321356683,版权所有2008 Sun Microsystems, Inc.。更多信息,请访问:www.informit.com,或者Safari Books Online的订阅者可以在这里阅读本书:http://safari.informit.com/9780321356680 |
引言
本章讨论对象的创建和销毁:何时以及如何创建它们,何时以及如何避免创建它们,如何确保它们被及时销毁,以及如何管理任何必须在其销毁前执行的清理操作。
条目 1:考虑使用静态工厂方法代替构造器
类正常情况下让客户端获取其实例的方式是提供公共构造器。还有一种技术应该成为每个程序员工具箱的一部分。类可以提供一个公共静态工厂方法,它只是一个返回类实例的静态方法。下面是`Boolean`(原始类型`boolean`的装箱类)的一个简单例子。这个方法将一个`boolean`原始值转换为一个`Boolean`对象引用。
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
请注意,静态工厂方法与《设计模式》中的工厂方法模式(Factory Method pattern)[Gamma95, p. 107]不同。本条目中描述的静态工厂方法在《设计模式》中没有直接的对应项。
类可以提供静态工厂方法来代替(或附加)构造器,供客户端使用。提供静态工厂方法代替公共构造器有利有弊。
静态工厂方法的一个优点是,与构造器不同,它们有名称。如果构造器的参数本身不能描述要返回的对象,那么具有良好命名选择的静态工厂会更容易使用,并且生成的客户端代码也更容易阅读。例如,返回一个可能为素数的`BigInteger`的构造器`BigInteger(int, int, Random)`,如果命名为静态工厂方法`BigInteger.probablePrime`会更好。(这个方法最终在1.4版本中添加了。)
一个类只能有一个具有给定签名的构造器。程序员们曾试图通过提供两个参数列表仅顺序不同的构造器来绕过这个限制。这是一个非常糟糕的主意。这种API的用户将永远记不住哪个构造器是哪个,并会错误地调用错误的构造器。阅读使用这些构造器的代码的人将无法知道代码的作用,除非查阅类文档。
由于静态工厂方法有名称,它们不受前一段所讨论限制的影响。在类似乎需要多个相同签名的构造器的情况下,用静态工厂方法和精心挑选的名称来代替构造器,以突出它们的区别。
静态工厂方法的第二个优点是,与构造器不同,它们不要求每次调用时都创建一个新对象。这使得不可变类(条目15)可以使用预先构造好的实例,或者缓存已构造的实例,并反复分发它们以避免创建不必要的重复对象。`Boolean.valueOf(boolean)`方法说明了这种技术:它从不创建对象。这种技术类似于享元模式(Flyweight pattern)[Gamma95, p. 195]。如果频繁请求等效对象,特别是如果它们创建起来很昂贵,它可以极大地提高性能。
静态工厂方法能够在重复调用时返回同一对象的能力,使得类能够严格控制任何时候存在的实例。这样做的类被称为实例控制类。编写实例控制类的原因有几个。实例控制允许类保证它是单例(强制执行s)或不可实例化(强制执行n)。此外,它允许不可变类(条目15)保证不存在两个相等的实例:`a.equals(b)`当且仅当`a==b`。如果一个类做出此保证,那么它的客户端可以使用`==`运算符而不是`equals(Object)`方法,这可能会提高性能。枚举类型(条目30)提供了此保证。
静态工厂方法的第三个优点是,与构造器不同,它们可以返回其返回类型的任何子类型的对象。这为您选择返回对象的类提供了极大的灵活性。
这种灵活性的一个应用是,API可以在不公开其类的情况下返回对象。以这种方式隐藏实现类可以使API非常简洁。这种技术适用于基于接口的框架(条目18),其中接口为静态工厂方法提供自然的返回类型。接口不能有静态方法,所以按照惯例,接口类型为`Type`的静态工厂方法被放在一个不可实例化的类(强制执行n)`Types`中。
例如,Java Collections Framework在其集合接口中提供了三十二个便捷实现,提供不可修改的集合、同步集合等。几乎所有这些实现都通过一个不可实例化类(`java.util.Collections`)中的静态工厂方法导出。返回对象的类都是非公共的。
如果Java Collections Framework导出三十二个单独的公共类(每个便捷实现一个),其API将比现在大得多。减少的不仅是API的数量,还有概念上的负担。用户知道返回的对象具有其接口精确指定的API,因此无需阅读实现类的其他类文档。此外,使用这种静态工厂方法需要客户端通过接口而不是实现类来引用返回的对象,这通常是一个好的做法(条目52)。
公共静态工厂方法返回的对象的类不仅可以是**非公共**的,而且根据静态工厂的参数值,类的类型也可以在调用之间发生变化。任何子类型于声明返回类型的类都是允许的。返回对象的类也可以在发布版本之间发生变化,以增强软件的可维护性和性能。
`java.util.EnumSet`类(条目32)在1.5版本中引入,没有公共构造器,只有静态工厂。它们根据底层枚举类型的大小返回两种实现之一:如果它有六十四个或更少的元素(大多数枚举类型如此),静态工厂将返回一个`RegularEnumSet`实例,它由一个`long`支持;如果枚举类型有六十五个或更多元素,工厂将返回一个`JumboEnumSet`实例,它由一个`long`数组支持。
这两种实现类的存在对客户端是不可见的。如果`RegularEnumSet`不再为小型枚举类型提供性能优势,它可以在未来的版本中被消除,而不会产生任何不良影响。同样,如果未来版本发现第三个或第四个`EnumSet`实现对性能有利,它也可以被添加。客户端不知道也不关心它们从工厂获得的对象的类;它们只关心它是否是`EnumSet`的某个子类。
静态工厂方法返回的对象类甚至可能在包含该方法的类编写时就不存在。这种灵活的静态工厂方法构成了服务提供者框架(service provider frameworks)的基础,例如Java数据库连接API(JDBC)。服务提供者框架是一个系统,其中多个服务提供者实现了服务,并且该系统将这些实现提供给其客户端,从而将它们与实现解耦。
服务提供者框架有三个基本组件:一个服务接口,由提供者实现;一个提供者注册API,系统使用它来注册实现,让客户端访问它们;以及一个服务访问API,客户端使用它来获取服务实例。服务访问API通常允许(但**不要求**)客户端指定一些选择提供者的标准。如果没有这样的规范,API将返回默认实现的实例。服务访问API是构成服务提供者框架基础的“灵活静态工厂”。
服务提供者框架的一个可选的第四个组件是服务提供者接口(service provider interface),提供者实现它来创建其服务实现实例。如果没有服务提供者接口,实现将通过类名注册并使用反射实例化(条目53)。在JDBC的情况下,`Connection`扮演服务接口的角色,`DriverManager.registerDriver`是提供者注册API,`DriverManager.getConnection`是服务访问API,而`Driver`是服务提供者接口。
服务提供者框架模式有许多变体。例如,服务访问API可以返回比提供者所要求的更丰富的服务接口,使用适配器模式(Adapter pattern)[Gamma95, p. 139]。下面是一个简单的实现,包含一个服务提供者接口和一个默认提供者。
// Service provider framework sketch
// Service interface
public interface Service {
... // Service-specific methods go
here
}
// Service provider interface
public interface Provider {
Service newService();
}
// Noninstantiable class for service registration and access
public class Services {
private Services() { } // Prevents instantiation (Enforce n)
// Maps service names to services
private static final Map<String, Provider> providers =
new ConcurrentHashMap<String, Provider>();
public static final String DEFAULT_PROVIDER_NAME = "<def>";
// Provider registration API
public static void registerDefaultProvider(Provider p) {
registerProvider(DEFAULT_PROVIDER_NAME, p);
}
public static void registerProvider(String name, Provider p){
providers.put(name, p);
}
// Service access API
public static Service newInstance() {
return newInstance(DEFAULT_PROVIDER_NAME);
}
public static Service newInstance(String name) {
Provider p = providers.get(name);
if (p == null)
throw new IllegalArgumentException(
"No provider registered with name: " + name);
return p.newService();
}
}
静态工厂方法的第四个优点是,它们减少了创建参数化类型实例的冗余。不幸的是,即使参数类型很明显,在调用参数化类的构造器时也必须指定类型参数。这通常需要您连续两次提供类型参数。
Map<String, List<String>> m =
new HashMap<String, List<String>>();
这种冗余的指定很快就会变得很麻烦,因为类型参数的长度和复杂性会增加。然而,使用静态工厂,编译器可以为您推断类型参数。这被称为类型推断。例如,假设`HashMap`提供了这个静态工厂。
public static <K, V> HashMap<K, V> newInstance() {
return new HashMap<K, V>();
}
那么,您可以用这个简洁的替代方案替换上面的冗长声明。
Map<String, List<String>> m = HashMap.newInstance();
总有一天,语言可能会在构造器调用和方法调用上都执行这种类型推断,但在1.6版本发布时,它还没有做到。
不幸的是,截至1.6版本,像HashMap这样的标准集合实现并没有工厂方法,但您可以在自己的实用工具类中添加这些方法。更重要的是,您可以在自己的参数化类中提供这样的静态工厂。
提供纯静态工厂方法的主要缺点是,没有公共或受保护构造器的类无法被子类化。对于公共静态工厂返回的非公共类也是如此。例如,无法子类化Collections Framework中的任何便捷实现类。可以说,这有时是件好事,因为它鼓励程序员使用组合而不是继承(条目16)。
静态工厂方法的第二个缺点是,它们不容易与其他静态方法区分开来。 它们不像构造器那样在API文档中脱颖而出,所以很难弄清楚如何实例化一个提供静态工厂方法而不是构造器的类。Javadoc工具可能有一天会引起对静态工厂方法的关注。在此期间,您可以通过在类或接口注释中强调静态工厂,并遵循通用的命名约定来减少这个缺点。以下是一些静态工厂方法的常见名称。
valueOf
——返回一个松散地说与参数具有相同值的实例。这种静态工厂实际上是类型转换方法。of
——`valueOf`的简洁替代,由`EnumSet`(条目32)推广。getInstance
——返回一个由参数描述但不能说具有相同值的实例。对于单例,`getInstance`不带参数并返回唯一的实例。newInstance
——类似于`getInstance`,不同之处在于`newInstance`保证每次返回的实例都与其他实例不同。getType
——类似于`getInstance`,但用于工厂方法在不同类中时。`Type`表示工厂方法返回的对象类型。newType
——类似于`newInstance`,但用于工厂方法在不同类中时。`Type`表示工厂方法返回的对象类型。
总之,静态工厂方法和公共构造器都有其用途,了解它们的相对优点是值得的。静态工厂通常更可取,所以不要在不先考虑静态工厂的情况下就本能地提供公共构造器。
条目 2:面对大量构造器参数时考虑使用Builder
静态工厂和构造器有共同的限制:它们对于大量可选参数扩展性不佳。考虑一个表示包装食品营养成分标签的类的例子。这些标签有几个必需的字段——份量、容器份数和每份卡路里——以及二十多个可选字段——总脂肪、饱和脂肪、反式脂肪、胆固醇、钠等等。大多数产品只有少数可选字段的值非零。
您应该为这样的类编写什么样的构造器或静态工厂?传统上,程序员使用**级联构造器模式**(telescoping constructor pattern),您提供一个只有必需参数的构造器,另一个带单个可选参数的构造器,第三个带两个可选参数的构造器,依此类推,直到最后一个带所有可选参数的构造器。实际情况是这样的。为简洁起见,只显示了四个可选字段。
// 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; // optional
private final int fat; // (g) optional
private final int sodium; // (mg) optional
private final int carbohydrate; // (g) 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);
通常,此构造器调用将需要您不想设置的许多参数,但您仍然必须为它们传递值。在这种情况下,我们为脂肪传递了0值。虽然“只有”六个参数可能看起来并不太糟糕,但随着参数数量的增加,情况很快就会失控。
简而言之,**级联构造器模式有效,但编写客户端代码非常困难,尤其是当参数很多时,而且更难阅读**。读者会想知道所有这些值代表什么,并且必须仔细计算参数才能找出。长串相同类型的参数可能导致微妙的错误。如果客户端意外地颠倒了两个这样的参数,编译器不会抱怨,但程序在运行时将行为异常(条目40)。
当您面临大量构造器参数时,第二个选择是**JavaBeans模式**,您调用一个无参构造器创建对象,然后调用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; // " " " "
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; }
}
这种模式没有级联构造器模式的任何缺点。创建实例很容易,尽管有点啰嗦,而且很容易阅读生成的代码。
NutritionFacts cocaCola = new NutritionFacts();
cocaCola.setServingSize(240);
cocaCola.setServings(8);
cocaCola.setCalories(100);
cocaCola.setSodium(35);
cocaCola.setCarbohydrate(27);
不幸的是,JavaBeans模式本身也有严重的缺点。由于构造是在多个调用之间分割的,因此JavaBean在构造过程中可能处于不一致的状态。类无法仅仅通过检查构造器参数的有效性来强制执行一致性。试图在对象处于不一致状态时使用它可能会导致与包含bug的代码相距甚远的故障,因此难以调试。相关的缺点是,JavaBeans模式排除了使类不可变(条目15)的可能性,并且需要程序员付出额外的努力来确保线程安全。
当其构造完成并且不允许使用时,可以通过手动“冻结”对象来减少这些缺点,但这是一种笨拙的变体,在实践中很少使用。此外,它可能导致运行时错误,因为编译器无法确保程序员在使用对象之前调用`freeze`方法。
幸运的是,还有第三种选择,它结合了级联构造器模式的安全性和JavaBeans模式的可读性。它是一种Builder模式[Gamma95, p. 97]的变体。客户端不是直接创建所需对象,而是调用带有所有必需参数的构造器(或静态工厂),然后获得一个*builder对象*。然后,客户端在builder对象上调用类似setter的方法来设置每个感兴趣的可选参数。最后,客户端调用一个无参的`build`方法来生成一个不可变的对象。Builder是它所构建的类的静态成员类(条目22)。实际情况是这样的。
// 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 carbohydrate = 0;
private int sodium = 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 carbohydrate(int val)
{ carbohydrate = val; return this; }
public Builder sodium(int val)
{ sodium = 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`是不可变的,并且所有参数的默认值都在一个位置。Builder的setter方法返回builder本身,以便可以链式调用。客户端代码看起来是这样的。
NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8).
calories(100).sodium(35).carbohydrate(27).build();
这种客户端代码易于编写,更重要的是,易于阅读。Builder模式模拟了Ada和Python等语言中命名的可选参数。
像构造器一样,builder可以对其参数施加不变量。`build`方法可以检查这些不变量。在将参数从builder复制到对象之后检查它们,并且在对象字段而不是builder字段上检查它们(条目39)至关重要。如果任何不变量被违反,`build`方法应抛出`IllegalStateException`(条目60)。异常的详细信息方法应指示违反了哪个不变量(条目63)。
施加涉及多个参数的不变量的另一种方法是让setter方法接受一组参数,其中必须满足某个不变量。如果不变量不满足,setter方法将抛出`IllegalArgumentException`。这的好处是,一旦传递了无效参数,就能检测到不变量失败,而不是等到调用`build`。
Builder相对于构造器的一个次要优点是,Builder可以有多个可变参数(varargs)。构造器(像方法一样)只能有一个可变参数。因为Builder使用单独的方法来设置每个参数,所以您可以拥有任意数量的可变参数,每个setter方法最多一个。
Builder模式是灵活的。单个Builder可以用于构建多个对象。Builder的参数可以在对象创建之间进行调整,以使对象多样化。Builder可以自动填充某些字段,例如在每次创建对象时自动递增的序列号。
一个设置好参数的Builder可以成为一个很好的抽象工厂(Abstract Factory)[Gamma95, p. 87]。换句话说,客户端可以将这样的Builder传递给一个方法,该方法可以为客户端创建一个或多个对象。为了支持这种用法,您需要一个类型来表示Builder。如果您使用的是1.5或更高版本,那么无论Builder构建什么类型的对象,一个通用的泛型类型(条目26)就足够了。
// A builder for objects of type T
public interface Builder<T> {
public T build();
}
请注意,我们的`NutritionFacts.Builder`类可以声明为实现`Builder<NutritionFacts>`。
采用Builder实例的方法通常会使用有界通配符类型(bounded wildcard type)(条目28)来约束Builder的类型参数。例如,这里有一个方法,它使用客户端提供的Builder实例为每个节点构建一个树。
Tree buildTree(Builder<? extends Node> nodeBuilder) { ... }
Java中传统的抽象工厂实现是`Class`对象,其中`newInstance`方法扮演着`build`方法的作用。这种用法充满了问题。`newInstance`方法总是尝试调用类的无参构造器,而该构造器可能根本不存在。如果类没有可访问的无参构造器,您将不会收到编译时错误。相反,客户端代码必须在运行时处理`InstantiationException`或`IllegalAccessException`,这既丑陋又不方便。此外,`newInstance`方法会传播无参构造器抛出的任何异常,即使`newInstance`没有相应的`throws`子句。换句话说,`Class.newInstance`破坏了编译时异常检查。上面的`Builder`接口纠正了这些缺陷。
Builder模式本身也有缺点。为了创建一个对象,您必须先创建它的builder。虽然创建builder的成本在实践中不太可能被注意到,但在某些性能关键的情况下可能是一个问题。此外,Builder模式比级联构造器模式更冗长,所以只有在参数足够多时(例如,四个或更多)才应该使用它。但请记住,您将来可能想添加参数。如果您从构造器或静态工厂开始,并且当类演变到参数数量开始失控时添加builder,那么过时的构造器或静态工厂会显得格格不入。因此,通常最好一开始就使用builder。
总之,**Builder模式是设计构造器或静态工厂参数超过少数几个(handful)的类的良好选择**,特别是当其中大多数参数是可选的时。与传统的级联构造器模式相比,使用builder编写和阅读客户端代码要容易得多,而且builder比JavaBeans安全得多。
条目 3:使用私有构造器或枚举类型强制实现单例属性
单例(singleton)就是一个只实例化一次的类[Gamma95, p. 127]。单例通常代表一个本质上是唯一的系统组件,例如窗口管理器或文件系统。**将一个类变成单例会使测试其客户端变得困难**,因为除非单例实现了一个充当其类型的接口,否则无法用模拟实现替换它。
在1.5版本发布之前,有两种实现单例的方法。两者都基于将构造器设为私有,并导出公共静态成员以提供对唯一实例的访问。一种方法是使用`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`实例存在——不多不少。客户端所做的任何事情都无法改变这一点,但有一个例外:特权客户端可以使用`AccessibleObject.setAccessible`方法通过反射(条目53)调用私有构造器。如果您需要防御这种攻击,请修改构造器,使其在被要求创建第二个实例时抛出异常。
在实现单例的第二种方法中,公共成员是一个静态工厂方法。
// Singleton with static factory
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public static Elvis getInstance() { return INSTANCE }
public void leaveTheBuilding() { ... }
}
所有对`Elvis.getInstance`的调用都返回相同的对象引用,并且永远不会创建其他Elvis实例(具有与上面相同的警告)。
公共字段方法的主要优点是声明清晰地表明该类是单例:公共静态字段是`final`的,所以它将始终包含相同的对象引用。公共字段方法不再有性能优势:现代Java虚拟机(JVM)几乎肯定会内联静态工厂方法的调用。
工厂方法方法的优点之一是,它允许您在不更改API的情况下灵活地改变主意,关于类是否应该是单例。工厂方法返回唯一实例,但可以很容易地修改为返回,例如,每个调用它的线程一个唯一的实例。第二个优点,关于泛型类型,在条目27中讨论。通常,这两个优点都不相关,而`final`字段方法更简单。
为了使使用上述任一方法实现的单例类可序列化(第11章),仅仅在声明中添加`implements Serializable`是不够的。为了维护单例保证,您必须将所有实例字段声明为`transient`并提供`readResolve`方法(条目77)。否则,每次反序列化一个序列化实例时,都会创建一个新实例,导致(在本例中)出现虚假的`Elvis`目击事件。为了防止这种情况,请将此`readResolve`方法添加到`Elvis`类中。
// readResolve method to preserve singleton property
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
在1.5版本发布时,有了实现单例的第三种方法。只需使一个枚举类型只有一个元素。
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
这种方法在功能上与公共字段方法相同,只是它更简洁,免费提供了序列化机制,并提供了防止多次实例化的坚固保证,即使面对复杂的序列化或反射攻击。虽然这种方法尚未被广泛采用,**但单个元素的枚举类型是实现单例的最佳方式**。
条目 4:使用私有构造器强制不可实例化
有时您可能想编写一个类,它只是静态方法和静态字段的集合。这类类声名狼藉,因为有些人滥用它们来避免以对象为中心思考,但它们确实有合法的用途。它们可以用于分组与原始值或数组相关的重载方法,类似于`java.lang.Math`或`java.util.Arrays`。它们还可以用于分组静态方法(包括工厂方法(参见s))到实现特定接口的对象中,类似于`java.util.Collections`。最后,它们可以用于分组final类上的方法,而不是扩展该类。
这类实用工具类不是为实例化而设计的:实例是没有意义的。然而,在没有显式构造器的情况下,编译器会提供一个公共的、无参的默认构造器。对用户而言,这个构造器与其他任何构造器都无法区分。在已发布的API中看到无意中可实例化的类并不少见。
**试图通过将类设为抽象来强制不可实例化是无效的**。该类可以被子类化,子类也可以被实例化。此外,它误导用户认为该类是为继承而设计的(条目17)。然而,有一个简单的技巧可以确保不可实例化。只有当一个类不包含显式构造器时,才会生成默认构造器,因此**通过包含一个私有构造器,类可以被设为不可实例化**。
// Noninstantiable utility class
public class UtilityClass {
// Suppress default constructor for noninstantiability
private UtilityClass() {
throw new AssertionError();
}
... // Remainder omitted
}
由于显式构造器是私有的,它在类外部是不可访问的。`AssertionError`不是必需的,但如果构造器意外地从类内部被调用,它提供了保险。它*保证*该类在任何情况下都不会被实例化。这个技巧有点违反直觉,因为构造器明确地提供,以便不能调用它。因此,明智的做法是包含一个注释,如上所示。
作为副作用,这种技巧也阻止了该类被子类化。所有构造器都必须显式或隐式地调用超类构造器,而子类将没有任何可访问的超类构造器可以调用。
条目 5:避免创建不必要的对象
复用单个对象而不是每次需要时都创建一个功能上等效的新对象通常是合适的。复用可以更快,也可以更优雅。如果对象是不可变的(条目15),它总是可以被复用。
作为不该做什么的极端例子,考虑这个语句。
String s = new String("stringette"); // DON'T DO THIS!
该语句每次执行时都会创建一个新的`String`实例,并且这些对象创建中的任何一个都是不必要的。`String`构造器的参数(“`stringette`”)本身就是一个`String`实例,功能上与构造器创建的所有对象都相同。如果这种用法出现在循环中或经常调用的方法中,可能会不必要地创建数百万个`String`实例。
改进后的版本是这样的。
String s = "stringette";
这个版本使用了一个`String`实例,而不是每次执行时都创建一个新的。此外,可以保证该对象将被运行在同一虚拟机的任何其他恰好包含相同字符串字面量的代码所复用[JLS, 3.10.5]。
您通常可以通过偏爱不可变类(immutable classes)的静态工厂方法(参见s)来代替构造器来避免创建不必要的对象。例如,`Boolean.valueOf(String)`静态工厂方法几乎总是比`Boolean(String)`构造器更可取。构造器每次调用时都会创建一个新对象,而静态工厂方法永远不需要这样做,并且实际上也不会这样做。
除了复用不可变对象外,如果您知道可变对象不会被修改,也可以复用它们。这是一个更微妙,也更常见的“不该做什么”的例子。它涉及可变`Date`对象,一旦其值被计算出来就永远不会被修改。这个类模拟了一个人,并有一个`isBabyBoomer`方法,用于判断这个人是否是“婴儿潮一代”,换句话说,这个人是否出生在1946年至1964年之间。
public class Person {
private final Date birthDate;
// Other fields, methods, and constructor omitted
// DON'T DO THIS!
public boolean isBabyBoomer() {
// Unnecessary allocation of expensive object
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
return birthDate.compareTo(boomStart) >= 0 &&
birthDate.compareTo(boomEnd) < 0;
}
}
`isBabyBoomer`方法每次调用时都会不必要地创建新的`Calendar`、`TimeZone`和两个`Date`实例。下面的版本通过静态初始化块避免了这种低效率。
class Person {
private final Date birthDate;
// Other fields, methods, and constructor omitted
/**
* The starting and ending dates of the baby boom.
*/
private static final Date BOOM_START;
private static final Date BOOM_END;
static {
Calendar gmtCal =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_START = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
BOOM_END = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START) >= 0 &&
birthDate.compareTo(BOOM_END) < 0;
}
}
改进后的`Person`类版本在它被初始化时只创建一次`Calendar`、`TimeZone`和`Date`实例,而不是每次调用`isBabyBoomer`时都创建。如果方法被频繁调用,这将带来显著的性能提升。在我的机器上,原始版本在1000万次调用中需要32000毫秒,而改进版本需要130毫秒,速度快了约250倍。不仅性能得到了提高,清晰度也得到了提高。将`boomStart`和`boomEnd`从局部变量更改为静态`final`字段,清楚地表明这些日期被视为常量,使代码更易于理解。为了充分披露,这种优化的节省并不总是如此显著,因为`Calendar`实例的创建特别昂贵。
如果Person类的改进版本被初始化,但其`isBabyBoomer`方法从未被调用,`BOOM_START`和`BOOM_END`字段将被不必要地初始化。可以通过在`isBabyBoomer`方法首次调用时惰性地初始化这些字段(条目71)来消除不必要的初始化,但不推荐这样做。正如惰性初始化经常发生的那样,它会使实现复杂化,并且除了我们已经取得的成就之外,不太可能带来明显的性能改进(条目55)。
在本项目前面的示例中,很明显所讨论的对象可以被复用,因为它们在初始化后不会被修改。在其他一些情况下,这一点不太明显。考虑适配器(adapters)[Gamma95, p. 139]的情况,也称为视图(views)。适配器是一个委托给后备对象的对象,为后备对象提供一个替代的接口。由于适配器除了其后备对象的状态之外没有其他状态,因此不需要为给定的对象创建多个适配器实例。
例如,`Map`接口的`keySet`方法返回`Map`对象的`Set`视图,由`Map`中的所有键组成。天真地看,似乎每次调用`keySet`都必须创建一个新的`Set`实例,但对给定`Map`对象调用`keySet`的每次调用都可能返回相同的`Set`实例。虽然返回的`Set`实例通常是可变的,但所有返回的对象在功能上都是等效的:当其中一个返回的对象发生变化时,所有其他对象也会发生变化,因为它们都由同一个`Map`实例支持。虽然为`keySet`视图对象创建多个实例是无害的,但也是不必要的。
1.5版本提供了一种创建不必要对象的新方法。它被称为自动装箱(autoboxing),它允许程序员混合原始类型和装箱类型,并在需要时自动进行装箱和拆箱。自动装箱模糊但并未消除原始类型和装箱类型之间的区别。存在微妙的语义区别,以及显而易见的性能差异(条目49)。考虑以下程序,它计算所有正`int`值的总和。为此,程序必须使用`long`算术,因为`int`不足以容纳所有正`int`值的总和。
// Hideously slow program! Can you spot the object creation?
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
该程序得到了正确的答案,但它的速度比应该的慢得多,原因是一个一字符的印刷错误。变量`sum`被声明为`Long`而不是`long`,这意味着程序构造了大约231个不必要的`Long`实例(大致对应于每次将`long i`添加到`Long sum`的次数)。将`sum`的声明从`Long`更改为`long`将运行时间从43秒减少到6.8秒(在我的机器上)。教训很清楚:优先使用原始类型而不是装箱类型,并注意意外的自动装箱。
本条目不应被误解为暗示对象创建很昂贵,应该避免。相反,创建和回收那些构造器做很少显式工作的细小对象是便宜的,特别是在现代JVM实现中。创建额外的对象以增强程序的清晰性、简洁性或功能通常是一件好事。
相反,通过维护自己的*对象池*来避免对象创建是一个坏主意,除非池中的对象非常重量级。对象池的一个典型例子是数据库连接。建立连接的成本足够高,因此复用这些对象是有意义的。此外,您的数据库许可证可能限制您只能连接固定数量的连接。但总的来说,维护自己的对象池会使代码混乱,增加内存占用,并损害性能。现代JVM实现拥有高度优化的垃圾收集器,在轻量级对象上,它们很容易胜过这样的对象池。
本条目与条目39(防御性复制)的对立面。避免创建不必要的对象说,“当您应该复用现有对象时,不要创建新对象”,而条目39说,“当您应该创建新对象时,不要复用现有对象”。请注意,在需要进行防御性复制时复用对象的惩罚远大于不必要地创建重复对象的惩罚。未能进行必要的防御性复制可能导致隐蔽的bug和安全漏洞;不必要地创建对象只会影响风格和性能。
当您从手动内存管理的语言(如C或C++)切换到垃圾回收语言时,程序员的工作会变得容易得多,因为当您不再使用对象时,它们会自动被回收。当您第一次体验它时,它看起来几乎像魔法。它很容易给人一种您不必考虑内存管理的印象,但这并不完全正确。
考虑以下简单的堆栈实现。
// Can you spot the "memory leak"?
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
return elements[--size];
}
/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
这个程序没有什么明显的问题(但请看条目26关于泛型版本)。您可以对其进行详尽的测试,它将以优异的成绩通过所有测试,但其中隐藏着一个问题。松散地说,程序存在“内存泄漏”,它可能悄无声息地表现为性能下降(由于垃圾收集器活动增加或内存占用增加)。在极端情况下,这种内存泄漏可能导致磁盘分页甚至程序因`OutOfMemoryError`而失败,但这种情况相对罕见。
那么,内存泄漏在哪里呢?如果一个堆栈先增长然后缩小,那么从堆栈中弹出的对象将不会被垃圾回收,即使使用堆栈的程序不再引用它们。这是因为堆栈维护着对这些对象的过时引用。过时的引用是指永远不会再次解除引用的引用。在这种情况下,`element`数组“活动部分”之外的任何引用都是过时的。活动部分由索引小于`size`的元素组成。
垃圾回收语言中的内存泄漏(更准确地称为无意的对象保留)是隐蔽的。如果一个对象引用被无意地保留,不仅该对象被排除在垃圾回收之外,而且该对象引用的任何对象也一样,依此类推。即使只保留了少数几个对象引用,也可能阻止许多、许多对象被垃圾回收,这可能会对性能产生重大影响。
解决这类问题的办法很简单:一旦引用过时,将其置`null`。在我们的`Stack`类的情况下,一旦引用从堆栈中弹出,它就变成了过时的。`pop`方法的更正版本如下所示。
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
将过时引用置`null`的一个额外好处是,如果随后意外地解除了对它的引用,程序将立即因`NullPointerException`而失败,而不是悄无声息地做错事。及早发现编程错误总是很有益的。
当程序员第一次遇到这个问题时,他们可能会过度补偿,只要程序不再使用某个对象引用就将其置`null`。这既不必要也不可取,因为它不必要地使程序混乱。**将对象引用置`null`应该是例外而不是常态**。消除过时引用的最佳方法是让包含引用的变量超出其作用域。如果您将每个变量定义在尽可能窄的作用域中,这一点就会自然发生(条目45)。
那么,您应该何时将引用置`null`?Stack类哪个方面使其容易发生内存泄漏?简而言之,它管理自己的内存。存储池由`elements`数组的元素组成(对象引用单元格,而不是对象本身)。数组活动部分中的元素是分配的,而数组其余部分的元素是空闲的。垃圾收集器不知道这一点;对垃圾收集器而言,`elements`数组中的所有对象引用都是同样有效的。只有程序员知道数组的非活动部分是不重要的。程序员通过手动将数组元素置`null`,一旦它们成为非活动部分,就有效地将此事实传达给垃圾收集器。
总的来说,**每当一个类管理自己的内存时,程序员都应该警惕内存泄漏**。每当一个元素被释放时,元素中包含的任何对象引用都应该被置`null`。
另一个常见的内存泄漏来源是缓存。一旦您将对象引用放入缓存,就很容易忘记它的存在,并将其留在缓存中,即使它已不再相关。有几种解决此问题的方法。如果您幸运地实现了一个缓存,其中一个条目在其键的引用(在缓存之外)存在时才相关,请将缓存表示为`WeakHashMap`;在条目过时后,它们将被自动删除。请记住,`WeakHashMap`仅在缓存条目的期望寿命由外部引用(而不是值)决定时才有用。
更常见的是,缓存条目的有用寿命定义不那么明确,条目随着时间的推移而变得价值降低。在这种情况下,缓存应偶尔清理已停用的条目。这可以通过后台线程(可能是`Timer`或`ScheduledThreadPoolExecutor`)完成,或者作为添加新条目到缓存的副作用。`LinkedHashMap`类通过其`removeEldestEntry`方法简化了后一种方法。对于更复杂的缓存,您可能需要直接使用`java.lang.ref`。
第三个常见的内存泄漏来源是监听器和其他回调。如果您实现的API允许客户端注册回调但未显式注销它们,那么除非您采取措施,否则它们将累积。确保回调被及时垃圾回收的最佳方法是只存储对它们(回调)的弱引用,例如,只将它们作为`WeakHashMap`的键存储。
由于内存泄漏通常不会表现为明显的故障,它们可能会在系统中存在多年。它们通常只有通过仔细的代码审查或借助称为堆分析器(heap profiler)的调试工具才能发现。因此,非常希望能够学会预测此类问题在发生之前,并防止它们发生。
条目 7:避免使用 finalizer
Finalizer 是不可预测的、通常是危险的,并且通常是不必要的。它们的使用可能导致异常行为、性能低下和可移植性问题。Finalizer 有一些有效用途,我们将在本条目后面讨论,但经验法则规定,您应该避免使用 finalizer。
C++程序员被警告不要将finalizer视为Java中C++析构函数的类似物。在C++中,析构函数是回收对象相关资源的正常方式,是构造函数的必要对应物。在Java中,当对象变得不可达时,垃圾收集器会回收与之相关的存储,而无需程序员付出任何特殊努力。C++析构函数也用于回收其他非内存资源。在Java中,`try-finally`块通常用于此目的。
Finalizer的一个缺点是*不能保证它们会被及时执行* [JLS, 12.6]。从对象变得不可达到其finalizer被执行之间可能需要任意长的时间。这意味着您*永远不应该在finalizer中执行任何时间关键的操作*。例如,依赖finalizer关闭文件是一个严重的错误,因为打开的文件描述符是有限的资源。如果由于JVM延迟执行finalizer而 left many files open,程序可能会因无法再打开文件而失败。
Finalizer执行的及时性主要是垃圾收集算法的函数,该算法在不同的JVM实现之间差异很大。依赖finalizer执行及时性的程序的行为也可能有所不同。完全有可能这样的程序在您测试它的JVM上运行良好,然后却在您最重要的客户喜欢的JVM上表现糟糕。
迟到的finalization不仅仅是一个理论问题。为类提供finalizer有时(在罕见情况下)会任意延迟其实例的回收。一位同事调试了一个长时间运行的GUI应用程序,该应用程序神秘地因`OutOfMemoryError`而崩溃。分析显示,在崩溃时,该应用程序的finalizer队列中有数千个图形对象等待被finalizer处理和回收。不幸的是,finalizer线程的优先级低于另一个应用程序线程,因此对象没有以它们被允许finalization的速度被finalizer处理。语言规范没有保证哪个线程将执行finalizer,因此除了避免使用finalizer之外,没有可移植的方法来防止此类问题。
语言规范不仅不保证finalizer会被及时执行;它也不保证它们会被执行。程序在不执行某些不可达对象的finalizer的情况下终止是完全可能的,甚至很有可能。因此,您*永远不应该依赖finalizer来更新关键的持久状态*。例如,依赖finalizer释放共享资源(如数据库)的持久锁是使整个分布式系统停滞不前的好方法。
不要被`System.gc`和`System.runFinalization`方法所诱惑。它们可能会增加finalizer执行的机会,但它们不能保证。声称保证finalization的唯一方法是`System.runFinalizersOnExit`及其邪恶的孪生兄弟`Runtime.runFinalizersOnExit`。这些方法存在致命缺陷,已被弃用[ThreadStop]。
如果您还没有被说服应该避免使用finalizer,这里还有另一个值得考虑的细节:如果在finalization期间抛出未捕获的异常,该异常将被忽略,并且该对象的finalization将终止[JLS, 12.6]。未捕获的异常可能使对象处于损坏状态。如果另一个线程试图使用这样一个损坏的对象,可能会导致任意的非确定性行为。通常,未捕获的异常将终止线程并打印堆栈跟踪,但在finalizer中发生时不会——它甚至不会打印警告。
哦,还有一件事:使用finalizer会带来*严重的*性能损失。在我的机器上,创建一个简单对象的创建和销毁时间约为5.6 ns。添加finalizer后,创建和销毁对象的时间会增加到2,400 ns。换句话说,使用finalizer创建和销毁对象的速度慢了约430倍。
那么,对于封装需要终止的资源(如文件或线程)的类的对象,您应该怎么做而不是编写finalizer?只需提供一个*显式的终止方法*,并要求类的客户端在不再需要每个实例时调用该方法。一个值得一提的细节是,实例必须跟踪它是否已被终止:显式终止方法必须在私有字段中记录该对象不再有效,并且其他方法必须检查该字段,并在对象被终止后调用时抛出`IllegalStateException`。
显式终止方法的典型示例是`InputStream`、`OutputStream`和`java.sql.Connection`的`close`方法。另一个例子是`java.util.Timer`的`cancel`方法,它执行必要的状态更改,以使与`Timer`实例关联的线程自行温和地终止。`java.awt`中的示例包括`Graphics.dispose`和`Window.dispose`。这些方法常常被忽略,并带来可预见的灾难性性能后果。一个相关的方法是`Image.flush`,它释放`Image`实例的所有资源,但将其保留在可以继续使用的状态,并在必要时重新分配资源。
*显式终止方法通常与try-finally结构结合使用,以确保终止*。在`finally`子句中调用显式终止方法可确保即使在使用对象时发生异常,它也会被执行。
// try-finally block guarantees execution of termination method
Foo foo = new Foo(...);
try {
// Do what must be done with foo
...
} finally {
foo.terminate(); // Explicit termination method
}
那么,finalizer有什么用,如果有的话?也许有两个合法的用途。一是作为“安全网”,以防对象的所有者忘记调用其显式终止方法。虽然不能保证finalizer会被及时调用,但在那些(希望很少)客户端未调用显式终止方法的情况下,晚点释放资源可能比从不释放要好。但是*finalizer应该在发现资源未终止时记录警告*,因为这表明客户端代码存在bug,应该修复。如果您正在考虑编写这样的安全网finalizer,请仔细考虑额外的保护是否值得额外的成本。
上面引用的四个类(`FileInputStream`、`FileOutputStream`、`Timer`和`Connection`)作为显式终止方法模式的示例,它们都有finalizer作为安全网,以防它们的终止方法未被调用。不幸的是,这些finalizer不会记录警告。通常无法在API发布后添加此类警告,因为它似乎会破坏现有客户端。
另一个合法的finalizer用途与具有本地对等体(native peers)的对象有关。本地对等体是一个本地对象,普通对象通过本地方法委托给它。因为本地对等体不是普通对象,所以垃圾收集器不知道它,并且在Java对等体被回收时无法回收它。Finalizer是执行此任务的合适工具,前提是本地对等体不持有关键资源。如果本地对等体持有必须及时终止的资源,那么该类应有一个显式的终止方法,如上所述。终止方法应执行任何必要的操作来释放关键资源。终止方法可以是本地方法,也可以调用本地方法。
重要的是要注意,“finalizer链”(finalizer chaining)不会自动执行。如果一个类(`Object`除外)有一个finalizer,并且子类重写了它,那么子类finalizer必须手动调用超类finalizer。您应该在`try`块中finalizer子类,并在相应的`finally`块中调用超类finalizer。这确保了即使子类finalization抛出异常,超类finalizer也会被执行,反之亦然。它看起来是这样的。请注意,这个例子使用了`Override`注解(`@Override`),它在1.5版本中被添加到平台中。您可以暂时忽略`Override`注解,或者查看条目36以了解它们的意思。
// 手动finalizer链@Override protected void finalize() throws Throwable {
try {
... // Finalize subclass state
} finally {
super.finalize();
}
}
如果子类实现者重写了超类finalizer但忘记调用它,那么超类finalizer将永远不会被调用。可以通过创建额外对象来抵御这种粗心或恶意的子类,每个要finalizer的对象一个。不要将finalizer放在需要finalizer的类上,而是将其放在一个匿名类(条目22)上,其唯一目的是finalizer其封闭实例。为封闭实例的每个实例创建一个匿名类的单个实例,称为finalizer guardian。封闭实例存储其finalizer guardian的唯一引用在一个私有实例字段中,因此finalizer guardian与封闭实例一样,都可以在同一时间有资格被finalizer。当guardian被finalizer时,它执行封闭实例所需的finalization活动,就像其finalizer是封闭类上的方法一样。
// Finalizer Guardian idiom
public class Foo {
// Sole purpose of this object is to finalize outer Foo object
private final Object finalizerGuardian = new Object() {
@Override protected void finalize() throws Throwable {
... // Finalize outer Foo object
}
};
... // Remainder omitted
}
请注意,公共类`Foo`没有finalizer(除了它从`Object`继承的那个微不足道的),所以子类finalizer是否调用`super.finalize`无关紧要。这项技术应该被考虑用于任何具有finalizer的非final公共类。
总之,除非作为安全网或用于终止非关键本地资源,否则不要使用finalizer。在那些罕见的需要使用finalizer的情况下,请记住调用`super.finalize`。如果您将finalizer用作安全网,请记住从finalizer中记录无效使用。最后,如果您需要将finalizer与公共的、非final类关联起来,请考虑使用finalizer guardian,这样即使子类finalizer未能调用`super.finalize`,finalization也可以发生。