一般来说,最好能重用单个对象,而不是在每次需要 的时候就创建一个相同功能的新对象 。 重用方式既快速,又流行 。 如果对象是不可变的( immutable ) (详见第 17 条),它就始终可以被重用 。
作为一个极端的反面例子,看看下面的语句 :
String s = new String("bikini"); // DON'T DO THIS!
该语句每次被执行的时候都创建一个新的 String 实例,但是这些创建对象的动作全都是不必要的。 传递给 String构造器的参数(bikini)本身就是一个 String 实例,功能方面等同于构造器创建的所有对象 。 如果这种用法是在一个循环中,或者是在一个被频繁调用的方法中,就会创建出成千上万不必要的 String 实例 。
改进后的版本如下所示 :
String s = "bikini";
这个版本只用了 一个 String 实例,而不是每次执行的 时候都创建一个新的实例 。 而且,它可以保证,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。
对于同时提供了静态 工厂方法( static factory method) 和构造器的不可变类,通常优先使用静态工厂方法而不是构造器,以避免创建不必要的对象 。 例如,静态工厂方法Boolean.valueOf(String )几乎总是优先于构造器 Boolean(String),注意构造器 Boolean(String)在 Java 9 中已经被废弃了 。 构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做,实际上也不会这样做 。 除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。
有些对象创建的成本比其他对象要高得多 。 如果重复地需要这类“昂贵的对象”,建议将它缓存下来重用 。 遗憾的是,在创建这种对象的时候,并非总是那么显而易见 。 假设想要编写一个方法,用它确定一个字符串是否为一个有效的罗马数字 。 下面介绍一种最容易的方法,使用一个正则表达式 :
这个实现的问题在于它依赖 String.matches 方法 。虽然String.matches 方法最易于查看一个字符串是否与正则表达式相匹配 , 但并不适合在注重性能的情形中重复使用 。 问题在于,它在内部为正则表达式创建了一个 Pattern 实例,却只用了一次,之后就可以进行垃圾回收了 。 创建Pattern实例的成本很高 ,因为需要将正则表达式编译成一个有限状态机( finite state machine)。
为了提升性能,应该显式地将正则表达式编译成一个 Pattern实例(不可变),让它成为类初始化的一部分,并将它缓存起来,每当调用 isRomanNumeral 方法的时候就重用同一个实例:
改进后的 isRomanNumeral 方法如果被频繁地调用,会显示出明显的性能优势。
如果一个对象是不变的,那么它显然能够被安全地重用,但其他有些情形则并不总是这么明显 。 考虑适配器( adapter )的情形,有时也叫作视图( view ) 。 适配器是指这样一个对象 :它把功能委托给一个后备对象( backing object ),从而为后备对象提供一个可以替代的接口 。 由于适配器除了后备对象之外, 没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例 。
例如 ,Map 接口 的 keySet 方法返回该 Map 对象的 Set 视图,其 中包含该 Map 中所有的键(key) 。 乍看之下 ,好像每次调用 keySet 都应该创建一个新 的 Set 实例,但是,对于一个给定的 Map 对象,实际上每次调用 keySet 都返回同样的 Set 实例 。 虽然被返回的 Set 实例一般是可改变的,但是所有返回的对象在功能上是等同的 :当其中一个返回对象发生变化的时候,所有其他的返回对象也要发生变化,因为它们是由 同一个 Map 实例支撑的 。 虽然创建 key Set 视图对象的多个实例并无害处, 却是没有必要,也没有好处的 。
另一种创建多余对象的方法,称作自动装箱( autoboxing ),它允许程序员将基本类型和装箱基本类型( Boxed Primitive Type )混用,按需要自动装箱和拆箱 。自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来, 但是并没有完全消除 。 它们在语义上还有着微妙的差别,在性能上也有着比较明显的差别 。 请看下面的程序,它计算所有int正整数值的总和 。 为此,程序必须使用 long 算法,因为 int 不够大,无法容纳所有 int 正整数值的总和:
private static long sum() {
Long sum = OL;
for (long i = 0; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
这段程序算出的答案是正确的,但是比实际情况要更慢一些,只因为打错了一个字符 。 变量 sum 被声明成 Long 而不是 long ,意味着程序构造了大约 231 个多余的 Long 实例(大约每次往 Long sum 中增加 long时构造一个实例) 。 将 sum 的声明从 Long 改成long ,在我的机器上使运行时间从 6.3 秒减少到了 0 . 59 秒 。 结论很明显 :要优先使用 基本类型而不是装箱基本类型,要当心无意识的自动装箱 。
不要错误地认为本条目所介绍的内容暗示着“创建对象的代价非常昂贵,我们应该要尽可能地避免创建对象” 。 相反,由于小对象的构造器只做很少量的显式工作,所以小对象的创建和回收动作是非常廉价的,特别是在现代的 JVM 实现上更是如此 。 通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事 。
反之,通过维护自己的对象池( object pool )来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的 。 正确使用对象池的典型对象示例就是数据库连接池 。 建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义 。 而且,数据库的许可可能限制你只能使用一定数量的连接 。 但是,一般而言,维护自己的对象池必定会把代码弄得很乱,同时增加内存占用( footprint ),并且还会损害性能 。 现代的 JVM 实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能 。