一般来说,最好是重用对象而不是在每次需要的时候就创建一个相同功能的新对象。重用方式既更加快速,也更为流行。如果对象是不可变的(immutable)(见第15条),它就始终可以被重用。
作为一个极端的反面例子,考虑下面的语句:
String s = new String("hello world"); // DON'T DO THIS!
该语句每次被执行的时候都创建一个新的String实例,但是这些创建对象的动作全都是不必要的。传递给String构造器的参数("hello world")本身就是一个String实例,功能方面等同于构造器创建的所有对象。如果这种用法是在一个循环中,或者是在一个被频繁调用的方法中,就会创建出成千上万不必要的String实例。
改进后的版本如下所示:
String s = "hello world";
这个版本只用了一个String实例,而不是每次执行的时候都创建一个新的实例。而且,它可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用[JLS, 3.10.5]。
对于同时提供了静态工厂方法(见第1条)和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。例如,静态工厂方法Boolean.valueOf (String)几乎总是优先于构造器Boolean(String)。构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做,实际上也不会这样做。
除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。下面是一个比较微妙、也比较常见的反例,其中涉及到可变的Date对象,它们的值一旦计算出来之后就不会再有变化。这个类建立了一个模型:其中有一个人,并有一个isBabyBoomer方法,用来检验这个人是否为一个"baby boomer(生育高峰期出生的小孩)",换句话说,就是检验这个人是否出生于1946至1964年间:
isBabyBoomer每次被调用的时候,都会新建一个Calendar、一个TimeZone和两个Date实例,这是没有必要的。下面的版本使用了一个静态的初始化器(initializer),避免了这种效率低下的情况:
改进后的Person类只在初始化的时候创建Calendar、TimeZone和Date实例一次,而不是在每次isBabyBoomer被调用的时候都创建这些实例。如果isBabyBoomer方法得到频繁的调用,这种方法将会显著地提高性能。在我的机器上,每调用一千万次,原来的版本需要32,000ms,而改进后的版本只需130ms,大约快了250倍。除了提高性能之外,代码的含义也更加清晰了。把boomStart和boomEnd从局部变量改为final静态域,这些日期显然是被作为常量对待,从而使得代码更易于理解。但是,这种优化带来的效果并不总是那么明显,因为Calendar实例的创建代价特别昂贵。
如果改进后的Person类被初始化了,它的isBabyBoomer方法却永远不会被调用,那就没有必要初始化BOOM_START和BOOM_END域。通过在isBabyBoomer方法第一次被调用的时候延迟初始化(lazily initializing)(见第71条)这些域,有可能消除这些不必要的初始化工作,但是不建议这样做。正如延迟初始化(lazy initialization)中常见的情况一样,这样做会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平(见第55条)。
在本条目前面的例子中,所讨论到的对象显然都是能够被重用的,因为它们被初始化之后不会再改变。其他有些情形则并不总是这么明显了。考虑适配器(adapter)的情形[Gamma95, p. 139],有时也称作视图(view)。适配器是指这样一个对象:它把功能委托给一个后备对象(backing object),从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。
例如,Map接口的keySet方法返回该Map对象的Set视图,其中包含该Map中所有的键(key)。粗看起来,好像每次调用keySet都应该创建一个新的Set实例,但是,对于一个给定的Map对象,每次调用keySet都返回同样的Set实例。虽然被返回的Set实例一般是可改变的,但是所有返回的对象在功能上是等同的:当其中一个返回对象发生变化的时候,所有其他的返回对象也要发生变化,因为它们是由同一个Map实例支撑的。虽然创建keySet视图对象的多个实例并无害处,却也是没有必要的。
在Java 1.5发行版本中,有一种创建多余对象的新方法,称作自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型(Boxed Primitive Type)混用,按需要自动装箱和拆箱。自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来,但是并没有完全消除。它们在语义上还有着微妙的差别,在性能上也有着比较明显的差别(见第49条)。考虑下面的程序,它计算所有int正值的总和。为此,程序必须使用long算法,因为int不够大,无法容纳所有int正值的总和:
这段程序算出的答案是正确的,但是比实际情况要更慢一些,只因为打错了一个字符。变量sum被声明成Long而不是long,意味着程序构造了大约2的31个多余的Long实例(大约每次往Long sum中增加long时构造一个实例)。将sum的声明从Long改成long,在我的机器上使运行时间从43秒减少到了6.8秒。结论很明显:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
不要错误地认为本条目所介绍的内容暗示着"创建对象的代价非常昂贵,我们应该要尽可能地避免创建对象"。相反,由于小对象的构造器只做很少量的显式工作,所以,小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。
反之,通过维护自己的对象池(object pool)来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。而且,数据库的许可可能限制你只能使用一定数量的连接。但是,一般而言,维护自己的对象池必定会把代码弄得很乱,同时增加内存占用(footprint),并且还会损害性能。现代的JVM实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。
与本条目对应的是第39条中有关"保护性拷贝(defensive copying)"的内容。本条目提及"当你应该重用现有对象的时候,请不要创建新的对象",而第39条则说"当你应该创建新对象的时候,请不要重用现有的对象"。注意,在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因创建重复对象而付出的代价。必要时如果没能实施保护性拷贝,将会导致潜在的错误和安全漏洞;而不必要地创建对象则只会影响程序的风格和性能。