术语:
延时初始化(lazily initializing):把对域的初始化延时到需要这些域的代码第一次被调用的时候进行。
适配器(adapter):它把功能委托给一个后备对象,从而为兵力对象提供一个可以替代的接口。
一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的实例,重用快速而又流行,如果对象是不可变的,那么它就始终可以被重用。作为一个反例,代码如下
String s = new String("test");
如果这条语句出现在循环中就会在第一轮中生成一个新的String实例,而这些String实例的功能完成一样,这会造成资源的巨大浪费,改进版本如下
String s = "test";
这个版本只用了一个String实例,而不是每次都创建一个新的实例,而且,它可以保证在所有同一台虚拟机上运行的代码,只要它们包含的字符串字面量相同,就可以保证该对象会被重用。
对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,原因在于构造器在每次被调用的时候都会创造一个新的对象,而静态工厂方法则不要求这样做,完成可以返回同一个对象的引用。
除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。下面这个例子是比较微妙,也比较常见的反面例子。
public class Person {
private final Date birthDate;
// Other fields, methods, and constructor ommitted
// 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实例,这是不必要的,因这它们一旦被创建即无需改变了。下面是一个用静态的初始化器的版本,避免了这种效率的情况。
public class Person {
private final Date birthDate;
// Other fields, methods, and constructor ommitted
/**
* 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);
Date boomStart = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Date boomEnd = gmtCal.getTime();
}
public boolean isBabyBoomer() {
return birthDate.compareTo(BOOM_START)>=0 && birthDate.compareTo(BOOM_END)<0;
}
}
改进后的Person类只在初始化的时候创建了Calendar, TimeZone和Date实例各一次,因此和上一种方法相比在isBabyBoomer方法被频繁调用的情况下,性能显著提高,除此之外,代码的含义也更加清晰了,把boomStart和boomEnd从局部变量改成final静态域,这些日期显然是被当作常量来对待,从而使代码更易于理解。但是这种优化带来的效果并不总是那么明显,为什么?答案是,因为Calendar实例的创建代价特别的昂贵。
这是还有一个问题,就是如果改进后Person类被初始化了,它的isBabyBoomer方法永远不会被调用,那么就没有必要初始化两个静态域了。通过延迟初始化则有可能消除这些不必要的初始化方法,但是不建议这样做。这样做使方法的实现变的更加复杂,从而无法将性能显著提高到超过已经达到的水平。
以上的例子都是非常明显的能判断出对象被初始化后无需改变的情况,但是还有一些情形表现的不是这么明显,例如考虑到适配器的情形(有时也叫做视图),由于适配器除了后备对象之个,没有其他的状态信息,所以针对某个给定的对象的特定适配器而言,它不需要创建多个适配器对象。
还有一种情况,在自动装箱(autoboxing)的机制下,这种机制允许程序员将基本类型和装箱基本类型混用,按需要自动装箱和拆箱。但是这两种类型在性能上有着比较明显的差别。考虑下面的程序,它计算所的int正传的总和。为些程序必需要使用long类型,因为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,Long是装箱基本类型,而long是基本类型,这样就在无意的情况了增加了拆装箱的开销,结论是:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
注意,小对象的构造器只做很少量的显示工作,所以,小对象的创建和回收是非常廉价的,特别是在现在的JVM上,通过创建附加的对象提升程序的清晰性、简洁性和功能性,这通常是件好事。反之,维护自己的对象池来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。比如说数据库连接池,建立数据库连接的代价非常昂贵,因为重用这些对象很有意义。而且,数据库的许可可能限制你只能使用一定数量的连接,尽管如此,维护对象池会把代码弄的很凌乱,难于管理,并且增加了内存的占用还会损害性能。现在的JVM实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。