一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。重用方式既快速、又流行。如果对象是不可变的(immutable),它始终可以被重用。
作为一个极端的反面例子,考虑下面的语句:
Strin g str=new String("HelloWorld");//不要这样做!
该语句每次执行的时候都创建一个新的String实例,但是这些创建对象的动作全都是不必要的。传递给String构造器的参数(“HelloWorld”)本身就是一个String实例,功能方面等同于构造器创建的所有对象。如果这种方法用在一个循环中,或者是在一个被频繁调用的方法中,就会创建出成千上万个不必要的String实例。
改进后的版本如下所示:
String str="HelloWorld";
这个版本只用了一个String实例,而不是每次执行的时候都创建一个新的实例。而且,它可以保证,对于所有在同一台虚拟机中运行的代码,只要他们包含相同的字符串字面常量,该对象就会被重用。
对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象。例如,静态工厂方法Boolean.valueOf(String)几乎总是优先于构造器Boolean(String)。构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做,实际上也不会这样做。
除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。下面是一个比较微妙、也比较常见的反面例子,其中涉及可变的Date对象,它们的值一旦计算出来之后就不再变化。这个类建立了一个模型:其中有一个人,并有一个isBabyBoomer方法,用来检验这个人是否一个“baby boomer(生育高峰期出生的小孩)",换句话说就是检验这个人是否出生在1946年至1964年期间。
public class Persion(){
private final Date birthDate;
//不要这样做
public boolen isBabyBoomer(){
Calendar gmtCal=Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
Date boomStart=gmtCal.getTime();
gmtCal.set(1964,1,0,0,0);
Date boomEnd=gmtCal.getTime();
return birthDate.compareTo(boomStart)>=0 && birthDate.compareTo<0;
}
}
isBabyBoomer每次调用的时候都会新建一个Calendar、一个TimeZone和两个Date实例,这是不必要的。下面的版本用一个静态的初始化器(initializer),避免了这种低效率的情况
class Person(){
private final Date birthDate;
private static final Date BOOM_START;
private static fianl Date BOOM_END;
static{
Calendar gmtCal=Calendar.getInstance(TimeZone("GMT"));
gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
BOOM_START=gmtCal.getTime();
gmtCal.set(1964,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实例一次,而不是每次调用isBabyBoomerd的时候都会创建这些实例。如果isBabyBoomer方法被频繁的调用,这种方法将会显著地提高性能。在我的机器上每调用一千万次,原来的版本需要32000ms,而改进后的版本只需130ms,大约快了250倍。除了提高性能之外,代码的含义也更加清晰了。把boomStart和boomEnd从局部变量改为final静态域,这些日期显然是作为常量对待,从而使得代码更容易理解。但是,这种优化带来的效果并不总是那么明显,因为Calendar实例的创建代价特别昂贵。
如果改进后的Person类被初始化了,它的isBabyBoomer方法却永远不会被调用,那就没有必要初始化BOOM_START和BOOM_END域。通过延迟初始化,即把对这些域的初始化延迟到isBabyBoomer方法第一次被调用的时候进行,则有可能消除这些不必要的初始化工作,但是不建议这么做。正如延迟初始化中常见的情况一样,这样做会使方法的实现更见复杂,从而无法将性能显著提高到超过已经达到的水平。
在本条目前面的例子中,所讨论到的对象显然都是能够被重用的,因为它们被初始化之后不会再改变。其他有些情形则并不总是这么明显了。考虑适配器的情形,有时也叫做视图。适配器是指这样一个对象:它把功能委托给一个后备对象,从而为后备对象提供一个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。
例如,Map接口的keySet方法返回该Map对象的Set视图,其中包含该Map中所有的键(Key)。大略看起来,好像每次调用keySet都应该创建一个新的Set实例,但是,对于一个给定的Map对象,实际上每次调用keySet都返回同样的Set实例。虽然被返回的Set实例一般都是可见,但是所有返回的对象在功能上都是等同的。当其中一个返回对象发生变化时,所有其他的返回对象也要发生变化,因为它们是由同一个Map实例支撑的。虽然创建keySet视图对象的多个实例并无害处,却也是没有必要的。
在java1.5发行版本中,有一种创建多余对象的新方法,称作自动装箱,它允许程序员将基本类型和装箱基本类型混用,按需要自动装箱、拆箱。自动装箱使得基本类型和装箱基本类型之间的差别变的模糊起来,但是并没有完全消除。它们在语义上还有微妙的差别,在性能上也有着比较明显的差别。考虑下面的程序,它计算所有int正值的总和。为此程序员必须使用long算法,因为int 不够大,无法容纳所有int正值的总是:
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,意味着程序构造了大约2的31次方多余的Long实例(大约每次往Long sum中增加long时构造一个实例)。将sum的声明从Long 改成long,在我的机器上运行时间从43秒减少到了6.8秒.结论很明显:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
不要错误的认为本条目所介绍的内容暗示着“创建对象的代价非常昂贵,我们应该要尽可能地避免创建对象”,相反,由于简单的对象构造器只做很少量的显示工作,所以,简单对象的创建和回收动作是非常廉价的特别是现代的JVM实现上更是如此。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。
反之,通过维护自己的对象池来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。真正正确使用对象池的典型示例就是数据库连接池,建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。而且数据库的许可可能限制你只能使用一定数量的连接。但是,一般而言,维护自己的对象池必定会把代码弄的很乱,同时增加内存占用,并且还会损害性能。现在的JVM实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。
关键字:java,对象,创建,销毁 | 来源:普索网 | 责任编辑:系统管理员 | 最后编辑:2011年08月16日 23时16分11秒