资讯专栏INFORMATION COLUMN

第6项:避免创建不需要的对象

Amio / 2068人阅读

摘要:传递给构造器的参数本身就是一个实例,功能方面等同于构造器创建的所有对象。对于同时提供了静态工厂方法第项和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,这样可以经常避免创建不必要的对象。

  一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。重用的方式既快速,有流行。如果对象是不可变(immutable)的(第17项),那么就能重复使用它。

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

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

  该语句每次被执行的时候都创建一个新的String实例,但是这些对象的创建并不都是必要的。传递给String构造器的参数("bikini")本身就是一个String实例,功能方面等同于构造器创建的所有对象。如果这种方法是用在一个循环中,或者是在一个被频繁调用的方法中,就会创建出成千上万的不必要的String实例。

  改进后的版本如下:

String s = "bikini";

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

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

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

// 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项说的是:你应该创建一个新的对象而不是重用一个已经存在的对象。注意,在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因创建重复对象而付出的代价。必要时如果没能实施保护性拷贝,将会导致潜在的错误和安全漏洞,而不必要地创建对象则只会影响程序的风格和性能。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77469.html

相关文章

  • Effective Java 三版 全文翻译

    摘要:本章中的大部分内容适用于构造函数和方法。第项其他方法优先于序列化第项谨慎地实现接口第项考虑使用自定义的序列化形式第项保护性地编写方法第项对于实例控制,枚举类型优先于第项考虑用序列化代理代替序列化实例附录与第版中项目的对应关系参考文献 effective-java-third-edition 介绍 Effective Java 第三版全文翻译,纯属个人业余翻译,不合理的地方,望指正,感激...

    galois 评论0 收藏0
  • Effective Java 3rd.Edition 翻译

    摘要:推荐序前言致谢第一章引言第二章创建和销毁对象第项用静态工厂方法代替构造器第项遇到多个构造器参数时要考虑使用构建器第项用私有构造器或者枚举类型强化属性第项通过私有构造器强化不可实例化的能力第项优先考虑依赖注入来引用资源第项避免创建不必要的对象 推荐序 前言 致谢 第一章 引言 第二章 创建和销毁对象 第1项:用静态工厂方法代替构造器 第2项:遇到多个构造器参数时要考虑使用构建器 第...

    KoreyLee 评论0 收藏0
  • 二章 创建和销毁对象

    摘要:一个类可以提供一个公共静态工厂方法,它仅仅是一第项遇到多个构造器参数时要考虑使用构建器静态工厂和构造器有个共同的局限性他们都不能很好地扩展到大量的可选参数。   本章涉及创建和销毁对象,包括何时以及如何创建它们,何时以及如何避免创建它们,如何确保它们被及时销毁,以及如何管理在销毁之前必须进行的清理操作。 第1项:用静态工厂方法代替构造器   类允许客户端获取实例的传统方法是提供公共构造...

    Jeffrrey 评论0 收藏0
  • 1:考虑静态工厂方法而是构造函数

    摘要:提供静态工厂方法而不是公共构造函数既有优点也有缺点。它们不像构造函数那样在文档中脱颖而出,因此很难弄清楚如何实例化提供静态工厂方法而不是构造函数的类。   类允许客户端获取实例的传统方法是提供公共构造器。还有一种技术应该是每个程序员的工具箱的一部分。一个类可以提供一个公共静态工厂方法,它仅仅是一个返回类实例的静态方法。下面是布尔(布尔型的盒装原语类)的一个简单示例。这个方法将一个布尔原...

    赵连江 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<