Effective Java 第三版——6. 避免建立没必要要的对象

Tips
《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必不少人都读过,号称Java四大名著之一,不过第二版2009年出版,到如今已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深入的变化。
在这里第一时间翻译成中文版。供你们学习分享之用。程序员

Effective Java, Third Edition

6. 避免建立没必要要的对象

在每次须要时重用一个对象而不是建立一个新的相同功能对象一般是恰当的。重用能够更快更流行。若是对象是不可变的(条目 17),它老是能够被重用。正则表达式

做为一个不该该这样作的极端例子,请考虑如下语句:数据库

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

语句每次执行时都会建立一个新的String实例,而这些对象的建立都不是必需的。String构造方法(“bikini”)的参数自己就是一个bikini实例,它与构造方法建立的全部对象的功能相同。若是这种用法发生在循环中,或者在频繁调用的方法中,就能够毫无必要地建立数百万个String实例。缓存

改进后的版本以下:安全

String s = "bikini";

该版本使用单个String实例,而不是每次执行时建立一个新实例。此外,它能够保证对象运行在同一虚拟机上的任何其余代码重用,而这些代码刚好包含相同的字符串字面量[JLS,3.10.5]。ide

经过使用静态工厂方法(static factory methods(项目1),能够避免建立不须要的对象。例如,工厂方法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方法。 虽然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微秒,速度提升了6.5倍。 性能上不只有所改善,并且更明确清晰了。 为不可见的Pattern实例建立静态final修饰的属性,并容许给它一个名字,这个名字比正则表达式自己更具可读性。

若是包含isRomanNumeral方法的改进版本的类被初始化,但该方法从未被调用,则ROMAN属性则不必初始化。 在第一次调用isRomanNumeral方法时,能够经过延迟初始化( lazily initializing)属性(条目 83)来排除初始化,但通常不建议这样作。 延迟初始化经常会致使实现复杂化,而性能没有可衡量的改进(条目 67)。

当一个对象是不可变的时,很明显它能够被安全地重用,可是在其余状况下,它远没有那么明显,甚至是违反直觉的。考虑适配器(adapters)的状况[Gamma95],也称为视图(views)。一个适配器是一个对象,它委托一个支持对象(backing object),提供一个可替代的接口。因为适配器没有超出其支持对象的状态,所以不须要为给定对象建立多个给定适配器的实例。

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

另外一种建立没必要要的对象的方法是自动装箱(autoboxing),它容许程序员混用基本类型和包装的基本类型,根据须要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。 有微妙的语义区别和不那么细微的性能差别(条目 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实现上。 建立额外的对象以加强程序的清晰度,简单性或功能性一般是件好事。

相反,除非池中的对象很是重量级,不然经过维护本身的对象池来避免对象建立是一个坏主意。对象池的典型例子就是数据库链接。创建链接的成本很是高,所以重用这些对象是有意义的。可是,通常来讲,维护本身的对象池会使代码混乱,增长内存占用,并损害性能。现代JVM实现具备高度优化的垃圾收集器,它们在轻量级对象上轻松赛过此类对象池。

这个条目的对应点是针对条目 50的防护性复制(defensive copying)。 目前的条目说:“当你应该重用一个现有的对象时,不要建立一个新的对象”,而条目 50说:“不要重复使用现有的对象,当你应该建立一个新的对象时。”请注意,重用防护性复制所要求的对象所付出的代价,要远远大于没必要要地建立重复的对象。 未能在须要的状况下防护性复制会致使潜在的错误和安全漏洞;而没必要要地建立对象只会影响程序的风格和性能。

相关文章
相关标签/搜索