Java性能优化(4):避免创建重复对象
重复使用同一个对象,而不是每次需要的时候就创建一个功能上等价的新对象,通常前者更为合适。重用方式既快速,也更为流行。如果一个对象时非可变的,那么它总是可以被重用。
作为一个极端的反面例子,考虑下面语句:
String s=new String("silly"); |
该语句每次被执行的时候都创建一个新的String实例,但是这些创建对象的动作没有一个是真正必需的。传递给String构造函数的实参(“silly”)本身就是一个String实例,功能上等同于所有被构造函数创建的对象。如果这种用法是在一个循环中,或者是在一个被频繁调用的方法中,那么成千上万不必要的String实例会被创建出来。
一个改进版本如下所示:
String s=”No longer silly”;
这个版本只使用一个String实例,而不是每次执行的时候创建一个新的实例。而且,他可以保证,对于所有在同一个虚拟机中运行的代码,只要它们包含相同的字符串字面常量,则该对象就会被重用。
对于同时提供静态工厂方法和构造函数的非可变类,你通常可以利用静态工厂方法而不是构造函数,以避免创建重复的对象。例如,静态工厂方法Boolean.valueOf(String)几乎总是优先于构造函数Boolean(String)。构造函数在每次被调用的时候都会创建一个新的对象,而静态工厂方法从来不要求这样做。
除了重用非可变的对象之外,对于那些已知不会被修改的可变对象,你也可以重用它们。下面是一个比较微妙、也比较常见的反例,其中涉及到可变对象,它们的值一旦被计算出来之后就不会再有变化。代码如下:
public class Person { |
isBabyBoom每次被调用的时候,都会创建一个新的Calendar、一个新的TimeZone和两个新的Date实例,这是不必要的。下面的版本用一个静态的初始化,避免了上面例子的低效率:
class Person { |
改进的person类仅在初始化时刻创建Calendar、TimeZone和Date实例一次,而不是在每次isBabyBoomer被调用的时候创建它们。如果isBabyBooner方法被频繁调用的话,则这将会带来显著的性能提高。在我的机器上,每调用一百万次,原来的版本需要36000ms。而改进的版本只需370ms,大约快乐100倍。除了性能提高之外,代码的含义也更加清晰了,把boomStart和boomEnd从局部变量改为final静态域,使这一点更加清晰:这些日期被作为常量对待,从而使得代码更加易于理解。但是,这种优化带来的效果并不总是那么明显,这里是因为Calendar实例创建代价特别昂贵。
如果isBabyBoomer方法永远也不会被调用,那么Person类的改进版本就没有必要去初始化BOOM_START和BOOM_END域。通过迟缓初始化将对这些域的初始化推迟到isBabyBoomer方法第一次被调用的时候,则有可能消除这些不必要的初始化工作,但不推荐这样做。如迟缓初始化中常见的情况一样,这样做会使方法的实现更加复杂,从而无法获取性能上的显著提高。
考虑适配器的情形,有时也被称为视图。一个适配器是指这样一个对象:它把功能委托给后面一对象,从而为后面的对象提供一个可选的接口。由于适配器除了后面的对象之外没有其他的状态信息,所以针对某个给定对象特定适配器而言,它不需要创建多个适配器实例。
例如map接口的keyset方法返回该map对象的set视图,其中包含该map中所有的键。粗看起来,好像每次调用keyset都应该创建一个新的set实例,但是对于一个给定map对象,每次调用keyset都返回同样的set实例。虽然被返回的set实例一般是可以改变的,但是所有返回的对象在功能上是等同的:当其中一个返回对象发生变化的时候,所有其他的返回对象也要发生变化,因为它们是由同一个map实例支撑的。
由于小对象的构造函数只做少量的工作,所以小对象的创建和回收动作是非常廉价的,特别是在现代的JVM实现上更是如此。通过创建附加的对象,以使得一个程序更加清晰简介。
一个正确使用对象池的例子就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这样的对象非常有意义。然而,一般而言,维护自己的对象池会把代码弄得很乱,增加内存占用,并且还会损害性能。现代的JVM实现有高度优化的GC机制,其性能很容易就会超过轻量级对象池的性能。
在提倡使用保护性拷贝的场合,因重用一个对象而招致的代价要远远大于创建重复对象而招致的代价。在要求保护性拷贝的情况下确定没有实施保护性拷贝,将会导致潜在的错误和安全漏洞:而不必要的创建对象仅仅会影响程序的风格和性能。