这通常是很合适去重用一个单例而不是去再创建另一个每一次使用都相同的功能性对象。重用资源可以更快也更加流行。同时一个不可变得对象总是能够被重用!(条目17)
这里有一个极端的例子,思考下面一条语句:
String s = new String("bikini"); // DON'T DO THIS!
这条语句每一次执行都创建了一个新的String实例,同时没有一个实例的创建是有必要的。这个String的构造器的参数"bikini"他自己就是一个String实例,功能上和所有调用构造器创建出来的实例是相同的。如果这种使用在一个循环的或者频繁地调用,成千上万的不必要的String实例就会被创建!
升级版是这样:
String s = "bikini"
这个版本使用了一个String实例而不是每一次就去创建一个String实例,除此之外,这样所会鼓励对象这个对象在其他运行在相同虚拟机的String对象能够被重用,因为虚拟机中能够包含这个字符串字面量!
你可能经常通过使用静态工厂方法(条目1)而不是构造器,同时仅向外提供私有构造器,来创建不可变对象以避免创建不必要的对象。例如,使用Boolean.valueOf就比Boolean的构造(在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();
}
}
这个升级版提供了一个更好的性能如果它频繁地被调用,在我的机器上,这个原始的版本使用了1.1 µs在一个8字符的输入字符串,而升级版只使用了0.17 µs。不只是更快。而且可以证明,这样所更加清晰,让一个静态filnal字段去存储一个课件的模式实例允许我们给他一个命名,这样有更好的可读性在正则表达式上。
如果这个类包含这个isRomanNumeral 的升级版是已经初始化过的,但是这个方法确实从来没有调用过,那么这个模式的字段将是一个没有必要去实例化的字段,我们有机会通过懒加载机制(条目83)来消除这个实例。他就编译一个没有可度量升级的实现。
当一个对象是不可变的,它明显可以被安全的重用,但是也可能有时候并不是那么明显甚至违反我们的直觉。思考这样一个适配器的问题,又被称为视图(views),一个适配器代表一个提供可以替代接口的的支持对象,因为一个适配器除了他的支持对象就没有状态,这里不需要再创建超过一个对象来传给适配器,因为他们需要的对象是相同的!
例如,Map接口的KeySet方法返回一个Map对象的Set视图,由所有在Map中的。natively,这看起来每一次调用KeySet都会不得不创建一个Set实例,但是每一次在Map接口上调用KeySet可能返回相同的Set实例,尽管返回的实例是一个典型的不可变化的对象,所有但会的对象功能上都是相同的。当一个返回的对象改变,所有其他的对象都会改变,因为他们都是来自于一个相同的Map实例。尽管通常在创建KeySet的时候多去创建一个实例是没有害处的,但是这样做其实也是没有必要也没有用处的。
另一个方式去创建不必要的对象是自动拆装箱,它允许程序员包装原始的类型成一个引用类型,以及一个对应的引用类型自动拆成原始类型。在原始类型和包装类型上有一些明显的语法区别,同时也有一些不那么微妙的不同。思考一下这样一个方法,它需要计算所有原始int的和,为了做这件事,程序不得不得使用一个long类型来存储所有int的值因为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;
}
这段程序可以得到一个正确的答案,但是它比它应该做到的更慢,这就是一个由于一个Long的一个单字节L本该是小写而写成大写造成的。这样意味着程序将进行2的31次方的不必要的Long的实例的生成(粗略地计算每一次会for会产生一个实例,其实java会对一些小的值进行缓存)。在我的机器上,将这个变量的声明从Long变为long可以将这段程序的运行时间从6.3s降低到0.59秒!这个问题就很清楚了,优先使用原始类型而不是包装类型。同时也要注意到不小心的自动拆装箱。
这个条目不应该被误解为暗示了对象创建时昂贵的、而是应该被避免!相反的是,一些小的对象的构造器只做了一些很少的事,这些对象的创建时很廉价的,尤其是在现代的JVM的实现上,创建一个多余的对象让程序更加清晰和简单,通常是一件更好的事。
相反的是,保持一个对象池来避免多余的对象的创建通常是一个很坏的idea!除非这些在池中的对象的创建是一个花费非常高的过程。典型的需要对象池的例子就是数据库连接。建立一个连接真的比确定来重新使用这个对象的代价更高。通常来说,无论如何,保持一个的对象池,会扰乱你的代码,会提高内存的占用,同时也会有一些不好的表现。现代的JVM实现已经有很好的优化垃圾回收能力很容易处理一些轻量级的对象。
这个条目的中心点时条目50的防止复制。现在的条目是说,“不要在你可以重用一个对象的时候创建一个新的对象。”记住,当不调用对象复制时,重复使用一个对象的惩罚是要远远好于创建一个不需要对象的的惩罚。如果防止对象出现失败的话,就可能会导致一个潜在的bug和安全问题,创建一个不必要的对象仅仅只会影响代码风格和性能。