Effective Java 第三版读书笔记——条款6:避免创建不必要的对象
通常来讲,重用一个对象比创建一个功能相同的对象更加合适。重用速度更快,并且更接近现代的代码风格。如果对象是不可变的(条款 17),它总是可以被重用。
考虑一个极端的例子:
String s = new String("bikini"); // DON'T DO THIS!
这个语句每次执行时都会创建一个新的 String 实例,而这些实例的创建都是不必要的。如果这种用法发生在循环或者频繁调用的方法中,就会创建数百万个毫无必要的 String 实例。
改进后的版本如下:
String s = "bikini";
该版本使用单个 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
实例就会被 JVM 进行垃圾回收。 创建 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
会使性能得到显著提升。 而且将不可见的 Pattern
实例显式创建允许我们给它起一个名字,这个名字通常比正则表达式本身更具可读性。
另一种创建不必要的对象的方式是自动装箱(autoboxing),它允许程序员混用基本类型和包装的基本类型,根据需要自动装箱和拆箱。 自动装箱模糊不清,但不会消除基本类型和装箱基本类型之间的区别。 考虑下面这个计算 int
范围内正整数总和的方法。 要做到这一点,程序必须使用 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
类型的 i
)。把 sum
变量的类型由 Long
改为 long
会使性能得到很大提升。这个教训很明显:优先使用基本类型而不是包装的基本类型,也要注意无意识的自动装箱。
这个条目不应该被误解为暗示对象创建是昂贵的,应该避免创建对象。 相反,创建和回收小的对象非常廉价,构造器只会做很少的工作,尤其在现代 JVM 实现上。 创建额外的对象以增强程序的清晰性,简单性或功能性通常是件好事。