一般来说,最好能重用单个对象,而不是在每次需要的时候就创建一个功能相同的新对象。重用的方式即快速,又流行。如果对象是不可变的,那么它就始终可以重用。
作为一个极端的反面例子,看看下面语句:
String s = new String("bikini"); // DON'T DO THIS!
// 这种方式不管内存中有没有bikini,都会新建一个bikini对象
该语句每次执行都会创建一个新的String实例,但是这些创建对象的动作全都是不必要的。传递给String构造器的参数("bikini")
本身就是一个String实例,功能方面等同于构造器创建的所有对象,这个例子本质上就是在用一个String实例去实例化另一个String。如果这种方法是在一个循环中,或者是在一个被频繁调用的方法中,就会创建大量无用的String实例。
改进后的版本如下:
String s = "bikini";
//这种方式先会在虚拟机中查找相同的字符串字面常量,如果有就直接引用,如果没有就新建一个bikini 对象,并指向它。
这个版本只用了一个String
实例,而不是每次执行的时候都创建一个新的实例。而且它可以保证,在同一台虚拟机运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。
对于同时提供了静态工厂方法和构造器的不可变类,通常优先使用静态工厂方法而不是构造器,避免创建不必要的对象。除了重用不可变的对象外,也可以重用那些已知不会被修改的可变对象。\
有些对象创建的成本比其他对象要高得多。如果重复的需要这类“昂贵的对象”,建议将它缓存下来重用。很遗憾的是,创建这种对象的时候,并非总是那么显而易见。例如,判断一个字符串是否为一个有效的罗马数字,最简单的方法是使用一个正则表达式:
static boolean isRomanNumeral(String s){
return s.**matches**("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
这个实例的问题在于它依赖String .matches
方法。虽然String .matches
方法最易于查看一个字符串是否与正则表达式相匹配,但并不适合在注重性能的情况下重复使用。问题在于它的内部为正则表达式创建了一个Pattern
实例,却只用了一次,然后就被回收了。创建Pattern
实例的成本很高,应为需要将正则表达式编译成一个有限状态机。String .matches
方法的源码:
public boolean matches(String regex) {
return **Pattern**.matches(regex, this);
}
为了提升新能,应该显示的将正则表达式编译成一个Pattern
实例(不可变),让它称为初始化的一部分,并将它缓存下载,每当调用isRomanNumeral
方法的时候就重用一个实例:
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
static boolean isRomanNumeral(String s){
return ROMAN.matcher(s).matches();
}
}
改进后的isRomanNumeral
在频繁调用的时候,会显现出明显的性能优势。
如果包含改进后的isRomanNumeral
方法的类被初始化了,但是该方法没有被调用,那就没有必要初始化ROMAN
域。通过在isRomanNumeral
方法第一次被调用的时候延迟初始化这个域,有可能消除这个不必要的初始化工作,但是不建议这样做。延迟初始化会是方法的实现更加复杂,从而无法将性能显著的提高到超过已经达到的水平。
如果一个对象是不可变的,那么它显然能够被安全的重用,但其他有些情形则并不是总是这么明显。考虑适配器的情形,有时候也叫视图。适配器是指这样的一个对象:它把功能委托给了一个后背对象,从而为后背对象提供一个可以替代的接口。由于适配器除了后背对象之外,没有其他的状态信息,所以针对某个给定的对象的特定适配器而言,它不需要创建多个适配器实例。
另一种创建多余对象的方法,称为**自动装箱,**它允许程序员将基本类型和基本装箱类型混用,按需自动装箱和拆箱。自动装箱使得基本类型和装箱基本类型之间的差异变得模糊起来,但是并没有完全消除。它在语义上还有微妙的差别,在性能上也有着比较明显的差别。请查看下面的程序,它计算所有int正整数的总和。为此,程序必须使用long算法,因为int不够大,无法容纳所有int正整数值得总和。
private static long sum(){
Long sum = 0L;
for (long i = 0; i<=Integer.MAX_VALUE;i++){
sum += i;
}
return sum;
}
这段程序算出得答案是正确得,但是实际情况要更慢一些,只因为打错了一个字符。变量sum被声明为Long而不是long,以为则程序造成了大约$2 ^ {31}$个多余的Long实例。将sum的Long改为long,从7.3秒减少到0.63秒。优先使用基本类型而不是装箱类型,要当心无意识的自动装箱。
我们要慎重的创建重量级的对象(例如数据库连接,建立数据库连接的代价时非常昂贵的),特别是频繁使用的对象,相反由于小对象只做很少的显示工作,所以小对象的创建和回收时非常廉价的,特别是现今的JVM具有高度优化的垃圾回收器。通过创建附加的对象,提升程序的清晰性、简洁性和功能性,这通常是件好事。
本条目对应的时第50条中有关“保护性拷贝”的内容。本条目提倡的时“当你应该重用对象的时候,请不要创建新的对象”,而第50条则说“当你应该创建新对象的时候,请不要重用现有的对象”。注意,在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因创建重复对象而付出的代价。必要时如果没能实施保护性拷贝,会导致潜在的Bug和安全漏洞;而不必要的创建对象只会影响程序的风格和性能。