本文是《Effective Java》读书笔记第5条。
一般来说,最好能重用对象,而不是在每次需要的时候就创建一个相同功能的新对象。如果对象是不可变的,那它就始终可以被重用。说到这,您可能立马就想到一个例子,没错,就是String。
String s = new String("asdf"); // 尽量不要这么做
语句每次执行的时候都会创建一个新的String实例,而且传递给构造方法的"asdf"
本身就是一个String实例,如果语句放到循环中,那么会创建很多没有必要的String实例。正确的方式是:
String s = "asdf";
这样的话能保证在一个虚拟机中,所有相同的字符串字面常量都指向同一个String对象(具体请见“Java创建字符串是用“”还是用构造器?”)。
静态方法
对于同时提供了静态工厂方法和构造器的不可变类,通常使用静态工厂方法而不是构造器,以免创造不必要的对象。
例如Integer的静态工厂方法valueOf
:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
通过代码可以看到,IntegerCache.low
和IntegerCache.high
之间的数对应的Integer实例是从缓存中取的,通常这个范围是-127~128,因为这个范围内的整数最容易被用到,如果采用缓存机制的话,那么每次调用valueOf(123)
返回的都是来自缓存中的同一个对象,从而避免了重复创建对象。当然这个范围是可以通过JVM参数设置的。
再举个例子,Map接口的keySet
方法返回该Map对象的Set视图,其中包括该Map中所有的键(key)。
public Set<K> keySet() {
Set<K> ks;
return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
}
对于一个给定的Map对象,每次调用keySet
都返回同样的Set实例,同样都是针对该Map的keySet视图,其中一个Set实例发生变化的时候,所有的其他Set也要发生变化,因为它们都是由一个Map实例支撑的。
自动装箱
自动装箱允许程序猿将基本类型和装箱基本类型混用,按需要自动装箱和拆箱,比如下边的代码:
int a = 123;
Integer b = 123;
int c = b;
a
是一个普通的int
基本类型变量,而b
是Integer对象,在赋值的时候便进行了自动装箱,类似的在将其赋值给c
的时候进行了自动拆箱。
自动装箱和拆箱是的基本类型和装箱基本类型之间的差别变得模糊起来,但没有完全消除,请看下边的例子:
Long sum = 0L;
for(long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
注意到,因为sum的类型被声明为Long
而不是long
,意味着程序构造了大约2^31个多余的Long实例(大约每次往Long sum中增加long时构造一个实例),将sum
的类型改为long
后,运行时间从6041ms减少到1161ms。因此:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱。
总结
尽量避免毫无意义的对象重复创建,而是尽量重用对象。
本文并非暗示“创建对象的代价非常昂贵,我们应该尽可能避免创建对象”。相反,对于小对象的创建和回收是非常廉价的,通过创建复杂的对象,提升程序的清晰性、简洁性和功能性,也是很重要的。
一般而言,自己来进行对象池的维护通常会导致代码混乱或复杂性增加,除非池中的对象是非常重量级的,比如数据库连接。现代的JVM实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级的对象池的性能。