避免创建不必要的对象

最常见的例子是String,一个小小的String对象看起来不起眼,但在复杂的大型系统中,如果不恰当创建,可能会带来极大的性能开销,比如:

String s = new String("stringette"); // DON'T DO THIS!

这行代码的问题是,作为参数的"stringette"本身已经是所需要的String对象了,菜鸟程序员却调用一个String类的构造函数来再次构造它。这行代码每执行一次,就会在栈里面建立一个一模一样的"stringette"对象。在高频执行的环境下,内存开销会非常大,甚至引起stack overflow。正确的做法是:

String s = "stringette";

这样建立的"stringette"对象,会由虚拟机的string pool机制管理,并存储于堆内存中。如果其他代码再用到这个字符串,同一个对象将被重用。比如:

String s1 = "stringette";
String s2 = "stringette";
String s3 = "stringette";

所有的s1,s2,s3,包括上面的s,实际上都指向内存中同一个对象,用==判断,他们全是相同的。所以不管多少次调用或重用,都只占用这一个对象的空间。但要注意,这种对象池思想,只应用于immutable对象,也就是说,可以这样重用的对象必须一经创建,就不再可以被修改。另一个类似的例子是Boolean对象的创建,推荐的方法是用静态factory方法:Boolean.valueOf(String),而不是Boolean(String),原理可以参看item 1

对于mutable对象,如果创建后不再改变,仍需要考虑对象的重用,以减少性能开销:

public class Person {
	private final Date birthDate;
	// Other fields, methods, and constructor omitted
	
	// 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对象,而且在多次调用中,建立的对象都是完全一样的,此时就需要把这样的对象重用起来:

class Person {
	private final Date birthDate;
	// Other fields, methods, and constructor omitted
	
	/**
	* 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);
		BOOM_START = gmtCal.getTime();
		gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
		BOOM_END = gmtCal.getTime();
	}
	
	public boolean isBabyBoomer() {
		return birthDate.compareTo(BOOM_START) >= 0 && birthDate.compareTo(BOOM_END) < 0;
	}
}

修改后的代码中,Calendar对象只被创建一次,在之后的isBabyBoomer调用中被重用。作为性能比较,原始代码执行1000万次耗时32000毫秒,而修改后的代码只用130毫秒,性能提升了250倍!类似的例子还有adapter设计模式中的后台对象,也应避免重复创建。

另一个产生冗余对象的场景是Java5的autoboxing特性:

// Hideously slow program! Can you spot the object creation?
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,所以循环中每次sum增值都会由autoboxing创建一个新的Long对象。循环结束时,这样的对象建立了221个!为避免这样的问题,要尽可能优先使用原始数据类型(int,long,double等等),而不是对象封装的数据类型(Integer,Long,Double等),还要留意autoboxing的出现

注意,本节内容不可过度应用,尤其是对象池思想,仅在对象创建开销非常大的情况下适用(比如管理数据库连接时),否则由于pool管理的复杂度,往往得不偿失。

提交评论


安全码
刷新