避免创建不必要的对象-第二章创建和销毁对象-Effective Java学习笔记05

文章内容来源于Joshua Bloch - Effective Java (3rd) - 2018.chm一书

第二章

创建和销毁对象

Item 6避免创建不必要的对象

Avoid creating unnecessary objects
重新使用单一个对象和创建一个新的对象,功能上等价。如果一个对象时不变的,那就能一直重用

下面是个错误例子:

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

上述陈述每次执行时创建一个新的不必的String对象
如果这种用法发生在循环或频繁调用的方法中,就会不必要地创建数百万个字符串实例
下述改进后的方法:

String s = "bikini";

用单个字符串实例,而不是每次执行时都创建一个新实例,此外,可以保证该对象将被运行在同一虚拟机中的任何其他代码重用,虚拟机中包含相同的字符串文本

在不可变类上都存在这两种方法的话,相比于使用构造函数更倾向于使用静态工厂方法来避免创建不必要的对象

例如,工厂方法Boolean.valueOf(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是匹配的最简单方法,但它不适合在性能关键的情况下重复使用。问题出现的原因是它在内部为正则表达式创建了一个模式实例,并且只使用了一次,之后就会进行垃圾收集。
创建模式实例代价高昂,因为它需要将正则表达式编译到有限状态机中
要提高性能,请将正则表达式显式编译为模式实例(不可变),作为类初始化的一部分,对其进行缓存,并在每次调用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();
    }
}

上述当一个对象是不可变的时,很明显它可以被安全地重用,但是在其他情况下它就不那么明显了,甚至是违反直觉的。

另一种创建不必要对象的方法是自动装箱,它允许程序员混合基本类型和装箱的基本类型,根据需要自动装箱和拆箱。自动装箱会模糊但不会消除基本体和装箱基本体类型之间的区别。

考虑下面的方法,它计算所有正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,这意味着程序构造了大约231个不必要的Long实例(每次long i被添加到Long sum中,大约一个实例)。
在我的机器上,将sum的声明从Long改为long将运行时间从6.3秒减少到0.59秒。教训很清楚:相比与装箱的原语,更倾向于原语;并注意无意的自动装箱。

总结

这个项目不应该被误解为意味着对象创建是昂贵的,应该避免。相反,创建和回收构造函数很少显式工作的小对象成本很低,尤其是在现代JVM实现中。

与此项相对应的是第50项防御性拷贝(保护性拷贝)。现在的条目说,“当你应该重用现有对象时不要创建新对象”,而条目50说,“当你应该创建新对象时不要重用现有对象。”

请注意,当需要防御性复制时重用对象的代价远远大于不必要地创建重复对象的代价。

猜你喜欢

转载自blog.csdn.net/weixin_43596589/article/details/112761397