Skip to content

Latest commit

 

History

History
75 lines (52 loc) · 6.16 KB

第6条:避免创建不必要的对象 2943dc5f56cc4637a4f3a813cc7b7436.md

File metadata and controls

75 lines (52 loc) · 6.16 KB

第6条:避免创建不必要的对象

一般来说,最好能重用单个对象,而不是在每次需要的时候就创建一个功能相同的新对象。重用的方式即快速,又流行。如果对象是不可变的,那么它就始终可以重用。

作为一个极端的反面例子,看看下面语句:

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和安全漏洞;而不必要的创建对象只会影响程序的风格和性能。