第6项:避免建立不须要的对象

  通常来讲,最好能重用对象而不是在每次须要的时候就建立一个相同功能的新对象。重用的方式既快速,有流行。若是对象是不可变(immutable)的(第17项),那么就能重复使用它。java

  做为一个极端的反面例子,考虑下面的语句:正则表达式

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

  该语句每次被执行的时候都建立一个新的String实例,可是这些对象的建立并不都是必要的。传递给String构造器的参数("bikini")自己就是一个String实例,功能方面等同于构造器建立的全部对象。若是这种方法是用在一个循环中,或者是在一个被频繁调用的方法中,就会建立出成千上万的没必要要的String实例。数据库

  改进后的版本以下:express

String s = "bikini";

  这个版本只用了一个String实例,而不是每次执行时都建立一个新的实例。除此以外,它能够保证,对于全部在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用 [JLS, 3.10.5]。缓存

  对于同时提供了静态工厂方法(第1项)和构造器的不可变类,一般可使用静态工厂方法而不是构造器,这样能够常常避免建立没必要要的对象。例如,这个静态工厂方法Boolean.valueOf(String)老是优先于在Java 9中抛弃的构造器 Boolean(String)。构造函数必须在每次调用时建立一个新对象,而工厂方法从不须要这样作,也不会在实践中。除了重用不可变对象以外,若是你知道它们不会被修改,你还能够重用可变对象。安全

  有些对象的建立比其余对象的代价大,若是你须要反复建立这种代价大的对象,建议将其缓存起来以便重复使用。不幸的是,当你建立这样一个对象时,并不老是很明显。假设你想编写一个方法来肯定一个字符串是不是一个有效的罗马数字。使用正则表达式是最简单的方法:ide

// Performance can be greatly improved!
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实例的代价很大,由于它须要将正则表达式编译为有限状态机(because it requires compiling the regular expression into a finite state machine)。函数

  为了提升性能,将正则表达式显式编译为Pattern实例(不可变)做为类初始化的一部分,对其进行缓存,并在每次调用isRomanNumeral方法时重用相同的实例:性能

// Reusing expensive object for improved performance
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能够显着提升性能。在个人机器上,原始版本在8个字符的输入字符串上须要1.1μs,而改进版本须要0.17μs,这是6.5倍的速度。不只提升了性能,并且功能更加明了。为不可见的Pattern实例建立一个静态的final字段,咱们能够给它一个名字,它比正则表达式自己更具备可读性。测试

  若是初始化包含改进版本的isRimanNumberal方法的类时,可是从不调用该方法,则不须要初始化字段ROMAN。在第一次调用isRimanNumberal方法时,能够经过延迟初始化字段(第83项)来消除使用时未初始化的影响,但不建议这样作。延迟初始化的作法一般都有一个状况,那就是它会把实现复杂化,从而致使没法测试它的性能改进状况。

  当一个对象是不可变的,那么就能够安全地重用它,可是在其余状况下,它并非那么明显,甚至违反直觉。这时候能够考虑使用适配器 [Gamma95],也称为视图。适配器是委托给支持对象的对象(An adapter is an object that delegates to a backing object),它提供一个备用接口。由于适配器的状态不超过其支持对象的状态,因此不须要为给定对象建立一个给定适配器的多个实例。

  例如,Map接口的keySet方法返回Map对象的Set视图,该视图由Map中的全部键组成。看起来,彷佛每次调用keySet都必须建立一个新的Set实例,可是对给定Map对象上的keySet的每次调用均可能返回相同的Set实例。尽管返回的Set实例一般是可变的,但全部返回的对象在功能上都是相同的:当其中一个返回的对象发生更改时,全部其余对象也会发生更改,由于它们都由相同的Map实例支持。虽然建立keySet视图对象的多个实例在很大程度上是无害的,但没必要要这样作,而且这样作没有任何好处。

  建立没必要要的对象的另外一种方式是自动装箱,它容许程序猿将基本类型和装箱基本类型(Boxed Primitive Type)混用,按需自动装箱和拆箱。自动装箱使得基本类型和装箱基本类型之间的差异变得模糊起来,可是并无彻底消除。它们在语义上还有微妙的差异,在性能上也有着比较明显的差异(第61项)。请考虑如下方法,该方法计算全部正整数值的总和,为此,程序必须使用long类型,由于int类型没法容纳全部正整数的总和:

// Hideously slow! Can you spot the object creation?
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实例(大约每次往Long sum中增长long时构造一个实例)。将sum的声明从Long改为long,在个人机器上运行时间从43秒减小到了6秒。结论很明显:要优先使用基本类型而不是装箱基本类型,要小心无心识的自动装箱。

  不要错误地认为本项所介绍的内容暗示着“建立对象的代价很是昂贵,咱们就应该尽量地避免建立对象”。相反,因为小对象的构造器只作不多量的显示工做,因此,小对象的建立和回收动做是很是廉价的,特别是在现代的JVM实现上更是如此。经过建立附加的对象,提高程序的清晰性、简洁性和功能性,这一般是件好事。

  反之,经过维护本身的对象池(object pool)来避免建立对象并非一种好的作法,除非池中的对象是很是重量级的。真正正确使用对象池的经典对象示例就是数据库链接池。创建数据库链接的代价是很是昂贵的,所以重用这些对象很是有意义。可是,一般来讲,维护本身的对象池一定会把代码弄得很乱,同时增长内存占用,并且会影响性能。现代的JVM实现具备高度优化的垃圾回收器,其性能很容易就会超太轻量级对象池的性能。

  与本项对应的是第50项的“保护性拷贝”的内容。该项说得是:你应该重用已经存在的对象,而不是去建立一个新的对象。然而第50项说的是:你应该建立一个新的对象而不是重用一个已经存在的对象。注意,在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因建立重复对象而付出的代价。必要时若是没能实施保护性拷贝,将会致使潜在的错误和安全漏洞,而没必要要地建立对象则只会影响程序的风格和性能。

我的公众号

相关文章
相关标签/搜索