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

避免创建不必要的对象

 

 

一般来说,最好能重用单个对象,而不是在每次需要的时候就创建一个相同功能的新对象。重用方式既快速,又流行。如果对象是不可变的(immutable)(详见第17条),它就始终可以被重用

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

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

语句每次执行时都会创建一个新的 String 实例,而这些对象的创建都不是必需的。String 构造方法("bikini") 的参数本身就是一个 bikini 实例,它与构造方法创建的所有对象的功能相同。如果这种用法发生在循环中,或者在频繁调用的方法中,就会毫无必要地创建数百万个 String 实例。

改进后的版本如下:

String s = "bikini";

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

 

对于同时提供了静态工厂方法( static factory method)(详见第1条)和构造器的不可变类,通常优先使用静态工厂方法而不是构造器,以避免创建不必要的对象。例如,静态工厂方法 Boolean.valueOf(String)比构造方法 Boolean(String) 更可取,注意构造器 Boolean(String)在Java9中已经被废弃了。构造器在每次被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做,实际上也不会这样做。除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。

 

有些对象创建的成本比其他对象要高得多。如果重复地需要这类“昂贵的对象”,建议将它缓存下来重用。遗憾的是,在创建这种对象的时候,并非总是那么显而易见。假设想要编写一个方法,用它确定一个字符串是否为一个有效的罗马数字。下面介绍一种最容易的方法,使用一个正则表达式

// 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 实例是昂贵的,因为它需要将正则表达式编译成有限状态机(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 微秒,而改进后的版本只花了 0.17 微秒,速度快了65倍。除了提高性能之外,可以说代码也更清晰了。将不可见的 Pattern实例做成 final 静态域时,可以给它起个名字,这样会比正则表达式本身更有可读性。

 

如果包含改进后的isRomanNumeral 方法的类被初始化了,但是该方法没有被调用,那就没必要初始化 ROMAN域。通过在 isRomanNumeral 方法第一次被调用的时候延退初始化( lazily initializing)(详见第83条)这个域,有可能消除这个不必要的初始化工作,但是不建议这样做。正如延迟初始化中常见的情况一样,这样做会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平(详见第67条)。

 

如果一个对象是不变的,那么它显然能够被安全地重用,但其他有些情形则并不总是这么明显。考虑适配器( adapter)的情形,有时也叫作视图(view)。适配器是指这样一个对象:它把功能委托给一个后备对象( backing object),从而为后备对象提供个可以替代的接口。由于适配器除了后备对象之外,没有其他的状态信息,所以针对某个给定对象的特定适配器而言,它不需要创建多个适配器实例。

 

例如,Map 接口的 keySet 方法返回 Map 对象的 Set 视图,包含 Map 中的所有 key。天真地说,似乎每次调用 keySet 都必须创建一个新的 Set 实例,但是对给定 Map 对象的 keySet 的每次调用都返回相同的 Set 实例。尽管返回的 Set 实例通常是可变的,但是所有返回的对象在功能上都是相同的:当其中一个返回的对象发生变化时,所有其他对象也都变化,因为它们全部由相同的 Map 实例支持。虽然创建 keySet 视图对象的多个实例基本上是无害的,但这是没有必要的,也没有任何好处。

 

另一种创建不必要的对象的方法是自动装箱(auto boxing),它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来,但是并没有完全消除。有微妙的语义区别和不那么细微的性能差异(详见第 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 ,这意味着程序构造了大约 231 不必要的 Long 实例(大约每次往 Long类型的 sum 变量中增加一个 long 类型构造的实例),把 sum 变量的类型由 Long 改为 long ,在我的机器上运行时间从 6.3 秒降低到 0.59 秒。这个教训很明显:优先使用基本类型而不是装箱的基本类型,也要注意无意识的自动装箱。

 

这个条目不应该被误解为暗示对象创建是昂贵的,应该避免创建对象。相反,使用构造方法创建和回收小的对象是非常廉价,构造方法只会做很少的显示工作,尤其是在现代 JVM 实现上。创建额外的对象以增强程序的清晰度,简单性或功能性通常是件好事。

 

反之,通过维护自己的对象池( object pool)来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。正确使用对象池的典型对象示例就是数据库连接池。建立数据库连接的代价是非常昂贵的,因此重用这些对象非常有意义。而且,数据库的许可可能限制你只能使用一定数量的连接。但是,一般而言,维护自己的对象池必定会把代码弄得很乱,同时增加内存占用( footprint),并且还会损害性能。现代的JVM实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。

 

与本条目对应的是第50条中有关“保护性拷贝”( defensive copying)的内容。本条目提及“当你应该重用现有对象的时候,请不要创建新的对象”,而第50条则说“当你应该创建新对象的时候,请不要重用现有的对象”。注意,在提倡使用保护性拷贝的时候,因重用对象而付出的代价要远远大于因创建重复对象而付出的代价。必要时如果没能实施保护性拷贝,将会导致潜在的Bug和安全漏洞;而不必要地创建对象则只会影响程序的风格和性能。

 

                                                                                    关注公众号、时刻获得更多内容

                                                             

感谢支持

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值