ITEM 6: 避免创建不必要的实例

ITEM 6: AVOID CREATING UNNECESSARY OBJECTS
  在每次需要时重用一个对象,而不是创建功能相同的新对象,是一种更合适的方案。重用效率更高。如果对象是不可变的,那么它总是可以重用的。
作为一个典型的反例,考虑下面的代码:
String s = new String("bikini"); // DON'T DO THIS!
  上面这条语句每次会创建一个新的String实例,然而这是不必要的。String 构造函数的参数(“bikini”)本身是一个String实例,在功能上与构造函数创建的所有对象相同。如果这种用法被用在循环或经常调用的方法中,则会不必要地创建数百万个String实例。
改进后的版本如下:
String s = "bikini";
  这个版本使用一个字符串实例,而不是每次执行时创建一个新的字符串实例。此外,这样做还能保证这个字符串对象将被运行在同一虚拟机中的任何其他代码重用,而该虚拟机恰好包含相同的字符串文本(常量池)。
  通过使用静态工厂方法而不是构造函数,可以避免创建不必要的实例。例如 Boolean.valueOf(String) 要优于 Boolean(String) (Java 9 中已废弃)。构造函数必须返回一个实例,而静态工厂方法没有被要求这么做,实践中常常也不会这么做。不可变对象是天然可重用的,此外如果是可变对象,如果你确认它们没有被改变,那么也是可重用的。
  有些对象实例化的开销非常高,如果我们需要频繁地实例化,那么可以考虑缓存它们以便重用。不幸的是,当我们写代码时并不是那么容易注意到这件事。假设我们想编写一个方法来确定字符串是否是有效的罗马数字,下面是使用正则表达式最简单的方法:

// 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,它是检测字符串是否匹配正则表达式的最简单的办法,但它并不适合在关注性能的场景下重复使用。问题在于,它在内部为正则表达式创建一个 Pattern 实例,并且只使用它一次,之后就将这个实例抛弃了,它将做为不再使用的对象被垃圾回收。因为需要对正则表达式进行编译,Pattern 实例化的代价比较高。为了提高性能,显式地编译正则表达式,作为类初始化的一部分并缓存它,在每次调用 isRomanNumeral() 方法时重用相同的 Pattern 实例:

// 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() 方法性能有很大提升。在我(作者)的机器上, 原始版本耗时1.1μs (8字节字符串),而改进的版本耗时0.17μs,快了6.5倍。不仅性能得到了改善,而且清晰度也得到了提高。我们可以给这个不可变的 Pattern 成员对象命名,这比正则表达式本身可读性强得多。
  如果 isRomanNumeral() 方法未被调用,那么 ROMAN 根本不需要初始化。我们可以通过延迟初始化来实现这一点,不过不建议这样做。与通常的延迟初始化一样,这将使实现变得复杂,并且没有可度量的性能改进。
  当一个实例是不可变得,那么毫无疑问它是能够安全重用的,但有些情况就不那么显而易见,甚至是违反直觉的。考虑一个adapters(适配器)的例子,适配器是这样一个对象,它代表一个委托的对象,对外提供一个可替代的接口。因为适配器是无状态的,所以不需要为给定对象创建多个给定适配器实例。例如,keySet() 方法为 Map 接口提供一个 Map 中所有 key 的集合视图。自然的,每次调用 keySet() 方法都应该返回一个新的 Set 实例,但对于一个指定的Map,每次调用 keySet() 都有可能返回相同的 Set 实例。虽然 Set 实例通常是可变的,但是在这个例子中所有返回的Set 实例在功能上是相同的: 当一个返回的对象发生更改时,所有其他对象也会发生更改,因为它们都由相同的Map实例支持。虽然创建 keySet 视图对象的多个实例在很大程度上是无害的,但这是不必要的,也没有好处。
  另一种创建不必要实例的场景时装箱/拆箱。自动装箱模糊了原始类型和装箱原始类型之间的区别,但并没有消除它们。它们有细微的语义差别和不那么细微的性能差别。考虑下面的方法,它计算所有正整数值的和。要做到这一点,程序必须使用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 实例(在每次 sum += i 时),在我(作者)的机器上,将 sum 的声明从 Long 更改为 long 可以将运行时从6.3秒减少到0.59秒。教训很明显的: 优先使用原语而不是包装类型,并且要注意无意的自动装箱。
  上面这个例子并不是在暗示创建对象是非常昂贵的,应该避免创建对象。恰恰相反,创建和回收小对象的成本很低,特别是在现代JVM上。创建额外的对象来增强程序的清晰度、简洁性或功能通常是一件好事。
  相反,通过维护自己的对象池来避免对象创建是一个坏主意,除非池中的对象非常重量级。一个使用对象池的经典例子是数据库连接,建立连接的成本非常高,因此有必要重用这些对象。但是,一般来说,维护自己的对象池会使代码混乱,增加内存占用,并损害性能。现代JVM实现具有高度优化的垃圾收集器,这些收集器在轻量级对象上很容易胜过此类对象池。
  这个条目的对应点是针对条目 50的防御性复制(defensive copying)。 当前项说,“当应该重用现有对象时,不要创建新对象”,而第50项说,“当应该创建新对象时,不要重用现有对象”。请注意,在需要防御性复制时重用对象的代价远远大于不必要地创建重复对象的代价。未能在需要的地方复制防御副本,可能会导致潜在的bug和安全漏洞;创建不必要的对象只会影响样式和性能。

你可能感兴趣的:(ITEM 6: 避免创建不必要的实例)