第5条:避免创建不必要的对象
一般来说,最好能重用的对象而不是在每次调用的时候就创建一个相同功能的新对象。重用方式既快速,又流行。如果对象是不可变的,它就始终可以被重用。
反例:
String s = new String("stringette");
该语句每次被执行的时候都创建一个新的String实例,但这些创建对象的动作全部都是不必要的。传递给String构造器的参数("stringette")本身就是一个String实例,功能方面等同于构造器创建的对象。如果这种用法实在一个循环中,或者在一个被频繁调用的方法中,就会创建出成千上万不必要的String实例。
改进后的版本如下:
String s = "stringette";
这个版本只用了一个String实例,而不是每一次执行代码都创建一个新的实例。
补充,享元模式与String类:JDK类库中的String类使用了享元模式,通过以下代码加以说明:
1 public static void main(String[] args) { 2 String str1 = "abcd"; 3 String str2 = "abcd"; 4 String str3 = "ab" + "cd"; 5 String str4 = "ab"; 6 str4 += "cd"; 7 8 System.out.println(str1 == str2); 9 System.out.println(str1 == str3); 10 System.out.println(str1 == str4); 11 12 str2 += "e"; 13 System.out.println(str1 == str2); 14 }
在Java语言中,如果每次执行类似String str1 = "abcd";的操作时都会创建一个新的字符串对象将导致内存开销很大,因此如果第一次创建了内容为"abcd"的字符串对象str1,下一次在创建内容相同的字符串对象str2时会将它的引用指向"abcd",不会重新分配内存空间,从而实现了"abcd"在内存中的共享。上述代码输出结果如下:
true true false false
可以看出,前两个输出语句均为true,说明str1、str2、str3、在内存中引用了相同的对象;而str4由于初值不同,在创建str4时重新分配了内存,所以第三个输出的结果为false;最后一个输出结果为false,说明当对str2进行修改时创建一个新的对象,修改工作在新的对象上完成,而原来引用的对象并没有发生任何变化,str1仍然引用原有对象,而str2引用新对象,str1与str2引用了两个完全不同的对象。
(1)对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创造不必要的对象。例如,静态工厂方法Boolean.valueOf(String)几乎总是优先于构造器Boolean(String)。构造器每次都会创建一个新的对象,而静态工厂方法则从来不要求这要做,也不会这样做。
(2)除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。示例如下:
一个类中涉及可变的Date对象,它们的值一旦被计算出来之后就不会再变化,这个类建立了一个模型:其中有一个人,并有一个isBabyBoomer方法,用来检测这个人是否为一个“baby boomer(生育高峰期出生的小孩)”,换句话说,就是检验这个人是否出生于1946年至1964年。
1 public class Person { 2 private final Date brithDate; 3 public Person(Date brithDate) { 4 this.brithDate = brithDate; 5 } 6 public boolean isBabyBoomer() { 7 //Unnecessary allocation of expensive object 8 Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); 9 gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0); 10 Date boomStart = gmtCal.getTime(); 11 gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0); 12 Date boomEnd = gmtCal.getTime(); 13 return brithDate.compareTo(boomStart) >= 0 && 14 brithDate.compareTo(boomEnd) < 0; 15 } 16 }
isBabyBoomer方法每次被调用时,都会新建一个Calendar、一个TimeZone和两个Date实例,这是不必要的。下面的版本用一个静态的初始化器(initializer),避免了这种效率低下的情况:
1 public class Person { 2 private final Date brithDate; 3 private final static Date BOOM_START; 4 private final static Date BOOM_END; 5 6 public Person(Date brithDate) { 7 this.brithDate = brithDate; 8 } 9 10 static { 11 Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); 12 gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0); 13 BOOM_START = gmtCal.getTime(); 14 gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0); 15 BOOM_END = gmtCal.getTime(); 16 } 17 18 public boolean isBabyBoomer() { 19 return brithDate.compareTo(BOOM_START) >= 0 && 20 brithDate.compareTo(BOOM_END) < 0; 21 } 22 }
改进后的Person类只在被初始化的时候创建Calendar、TimeZone和Date的实例一次,而不是每次在调用isBabyBoomer方法的时候都创建这些实例。在调用计算次数特别多的时候,大大的减少了代码的运行时间,显著的提高了性能。除此之外,代码的含义也更加清晰了。
(3)在前面的例子中,所讨论到的对象显然都是能够被重用的,因为他们初始化之后不会再改变,其他有些情形则并不这么明显了。考虑适配器(adapter)的情形,有时也叫视图(view)。适配器是指这样一个对象:它把功能委托给一个后备对象(backing object),从而为后备对象提供一个可以代替的接口。由于适配器除了后备对象之外,就没有其它的状态信息,所以针对某个给定的特定适配器而言,它不需要创建多个适配器实例。
例如,Map接口的KeySet方法返回该Map对象的Set视图,其中包含Map中所有的键(key)。粗看起来,好像每次调用KeySet都应该创建一个新的Set实例,但是,对于一个给定的Map对象,实际上每次调用KeySet都返回同样的Set实例。虽然被返回的Set实例一般是可改变的,但是所有返回的对象在功能上是等同的:当其中一个返回对象发生改变的时候,所有其它的返回对象也要发生变化,因为它们是有同一个Map对象支撑的。虽然创建KeySet视图对象的多个实例并无害处,但却也没必要。
在Java中,有一种创建多余对象的新方法,成为自动装箱(autoboxing),它允许程序员将基本类型和装箱基本类型(Boxed Primitive Type)混用,按需求自动装箱和拆箱。
1 public static void main(String[] args) { 2 Long sum = 0L; 3 long time = System.currentTimeMillis(); 4 for(long i = 0; i < Integer.MAX_VALUE; i++) { 5 sum += i; 6 } 7 time = System.currentTimeMillis() - time; 8 System.out.println(time/1000.0); 9 System.out.println(sum); 10 }
这段程勋的答案是正确的,但是比实际情况更慢一些,只因为打错了一个字符,变量sum被声明称Long而不是long,以为着程序构造了大约2^31个多余的Long实例。将sum上午声明从Long变成long,在我机器上运行的时间从8.952秒减少到了0.821秒。结论很明显,要优先考虑基本类型而不是装箱基本类型,要当心无意识地装箱。
(4)对象重用在数据库连接池中非常有意义。