创建销毁对象(第六条:避免创建不必要的对象)

通常来说,比起每次需要都去创建一个新的功能相同的对象,重用一个对象是更适合的。重用会让程序更快而且代码更好看。如果对象是不变的,那么它一直都是可以重用的(Item 17)。

就不要去做的一个极端的例子,考虑下面的代码:

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

这个代码每次执行都会创建一个新的String实例,然而没有一次创建时必要的。String构造器constructor ("bikini" )它的参数本身就是一个String对象,功能来说用构造器构造出来的所有对象跟这个参数本身都是相同的。如果这种用法在一个循环方法中或者在一个频繁调用的方法中,成百上千万不必要的String对象都会被创建出来。

改良的版本由下面所示:

Strings = "bikini";

这种方法用单个的String实例而不是每次执行都创建一个新的实例。另外,它可以保证只要在相同的虚拟机中,任何碰巧包含相同的字符文本的代码被调用时,这个对象就会被重用。

不变的类是可以提供静态工厂方法还有构造方法的,为了避免创建不必要的对象,通常使用静态工厂方法(Item 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之所以很贵是因为它需要将正则表达式编译到有限状态机中。

为了提高性能,可以显示的将正则表达式编译到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微秒,快了6.5倍。提升不不仅仅是性能,还有值得争论的是,清晰性。给Pattern的实例一个静态的final类型的属性可以让你给它一个名字,名字比起正则表达式本身而言可读性可高多了。如果不这样做,Pattern的实例压根就看不到。如果改良版本的isRomanNumeral方法被初始化了,可是这个方法从没有被用过,那么这个ROMAN属性就被初始化的不必要。尽管可以通过懒初始化(Item 83)这个属性来消除这个问题,但是这是不推荐的。正如通常的懒初始化的情况,这种无法验证的性能改良(Item 67)会使实现变得复杂。当一个对象被设置为不可变,那么它能被安全的重复使用是众所周知的。但是存在一些情况,能不能安全的重复使用一点也不明显,甚至是违反直觉的。考虑adapters,也以views被人所熟知,的情况。Adapter是一种对象,这种对象会给背后的对象委派一个替代的接口。由于adapter是没有所支持对象的状态以外的任何状态,对于一个给定的对象的adapter,是没有必要创建多于一个实例的。比如Map接口里面的keySet方法,它返回Map对象中由所有的keys组成的一个set。很容易简单认为,每次调用某个给定的Map对象的keySet会创建一个新的set对象,但是其实每次调用某个给定的Map对象的keySet会返回相同的set实例。尽管返回的set实例通常是可变的,但是所有的返回对象在功能上都是相同的:当返回对象中有改变的,其他返回的对象也会改变,因为他们都是由一个Map实例来存储的。尽管大体上来讲创建多个keySet视图对象是无伤大雅的,但是这样做是没有必要的并且没有好处的。

另一种创建不必要的对象的方式是自动装箱,如有需要它会自动装箱拆箱。它会允许开发人员混合使用原始类型还有装箱的原始类型。自动装箱使得原始类型还有装箱类型变得模糊了,但是它并没有消除二者的差别。他们语义上的差别是很隐晦的,但是性能上的差别却不隐晦(Item 61)。考虑下面计算所有正的int值总和的方法。为了完成这个,程序用long来做计算,因为int来存储所有的正的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 i被添加到Long sum)。在我的机器上将sum的类型从Long改到long将运行时间从6.3秒降低到了0.59秒。教训是很明显的:比起装箱类型而言要更推崇原始类型,小心无心的装箱

这章的内容不应该被曲解为,创建对象是很消耗的并且应该避免掉。相反,构造方法只做一点点事的小的对象的创建还有回收还是很轻松的,尤其在基于现代的JVM的实现上。通过创建额外的对象来增强清晰度,简单性或者程序的能力通常是一个好事。

相反的,通过维护自己的对象池来避免创建对象是一个坏主意,池里的对象创建起来确实很重量级。验证对象池的经典的例子的对象是数据库连接对象。由于创建连接的消耗确实很高,所以重复使用这些对象还是很有意义的。总而言之,维护自己的对象池会使代码变得混乱,增加内存足迹,而且损害性能。

跟这一条相对应的是 Item 50 的防御性复制。这一条说,“在你应该利用一个存在的对象的时候,不要创建一个新的对象”然而 Item 50 说,“当你需要创建一个新的对象的时候,不要重复使用已经存在的对象。”注意,当需要防御性复制时,比起创建多于的对象,如果重复使用了一个存在的对象,那么它所带来惩罚要大得多。当需要防御性复制却没做到的时候,会导致隐含的 bugs 还有安全漏洞;不必要的去创建对象只会影响代码样子还有性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值