第二章 创建和销毁对象
第六条 避免创建不必要的对象
最好能重用单个对象,而不是在每次需要的时候就创建一个相同功能的新对象。
如果对象是不可变的,它就可以始终被重用。
一 例子
先看一个极端的反例:
//一个极端的反例
String s = new String("bikini");//比基尼
该语句每次执行都会创建一个新的String对象,但是这是不必要的。
传递给String构造方法的参数"bikini"本身就是一个String对象。
改进后:
//改进后的版本
String s = "bikini";
这里只用了一个String对象,而不是每次执行都创建一个新对象。
而且,对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用(静态常量池)。
二 优先使用静态工厂方法
对于同时提供了构造方法和静态工厂方法的不可变类,优先使用静态工厂方法以避免创建不必要的对象。
例如:
//静态工厂方法 更推荐使用
Boolean.valueOf(String);
//构造方法 在Java9中已经被废弃了
Boolean(String);
三 将经常创建的对象缓存
如果需要重复地创建某类“昂贵的对象”,可以将它缓存下来重用。
下面是一个罗马数字校验器类,确定一个字符串是否为一个有效的罗马数字:
/*罗马数字校验器*/
public class RomanNumerals {
//使用一个正则表达式
static boolean isRomanNumeral(String s) {
return s.matches("^(?=.)M*(C[MA]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
}
虽然String.matches(String regex)
方法最易于查看一个字符串是否与正则表达式相匹配,但并不适合在注重性能的情形中重复使用:
//String.java 源码
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
//Pattern.java 源码
public static boolean matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex); //问题所在
Matcher m = p.matcher(input);
return m.matches();
}
每次调用这个方法都会创建一个Pattern对象,却只用了一次就被垃圾回收了,这样很浪费性能。
为了提升性能,应该将正则表达式缓存起来,便于重用:
/*罗马数字校验器*/
public class RomanNumerals {
//有私有静态不可变的Pattern将正则表达式缓存起来
private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MA]|D?C{0,3})" + "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
//每次调用时会重用同一个ROMAN实例,大大提升性能
static boolean isRomanNumeral(String s) {
return ROMAN.matcher(s).matches();
}
}
此处用的是饿汉式,即类加载完成时ROMAN就已经被初始化了。书中提到不推荐使用懒汉式,因为这样会使方法的实现更加复杂,从而无法将性能显著提高到超过已经达到的水平(详见第67条)(现在不是很懂)。
四 警惕无意识的自动装箱
自动装箱允许程序员将基本类型和装箱基本类型混用,按需要自动装箱和拆箱。
自动装箱使得基本类型和装箱基本类型之间的差别变得模糊起来,但是并没有完全消除。
例如下面是一个计算所有int正整数值总和的方法(反例):
//计算所有int正整数值总和
private static long sum() {
//int无法容纳所有int正整数的总和,所以使用long类型
Long sum = 0L; //所有int正整数的总和
//↑问题所在(注意这里是Long不是long)
for(long i = 0; i <= Integer.MAX_VALUE; i++) {
sum += i;
}
return sum;
}
在这个方法中,变量sum被声明成Long而不是long,意味着程序创建了大约231个多余的Long对象。这将导致运行时间大大增加,严重影响效率。
因此,要优先使用基本类型而不是装箱基本类型,要警惕无意识的自动装箱。
五 并非任何时候都要避免创建对象
通过创建附加的对象,提升程序的清晰性、简洁性和功能性是件好事;通过维护自己的对象池来避免创建对象会把代码弄得很乱,同时增加内存占用和损害性能。
现代JVM实现具有高度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。
必要时如果没能实施保护性拷贝,将会导致潜在的Bug和安全漏洞;而不必要地创建对象则只会影响程序的风格和性能。(详见第50条)
六 总结
一般应该避免创建不必要的对象,可以通过优先使用静态工厂方法、将经常创建的对象缓存、优先使用基本类型而不是装箱基本类型等方式来避免。
但是要注意并非任何时候都要避免创建对象。