[关闭]
@XingdingCAO 2017-12-13T14:00:55.000000Z 字数 4076 阅读 1793

Effective Java(第2版):Item 5 —— Avoid creating unnecessary objects

Java constructor


实例的重用

通常情况下,对一个物体的重复利用是优于反复创建等效新物体的。重用利用不仅更快而且拥有更好的格式。

通过静态工厂方法(Item 1)来实现重用

——在Item 1中讨论过的静态工厂方法有一个特点——实例化可控,这样就可以返回相同实例的引用。而构造方法则必须在每次调用时生成新的实例。
——例如,Boolean类中的静态工厂方法valueOf(String)和构造方法Boolean(String)。前者返回了类中的静态常量。而后者必然构造了新的实例。

  1. public static boolean parseBoolean(String s) {
  2. return ((s != null) && s.equalsIgnoreCase("true"));
  3. }
  1. public static final Boolean TRUE = new Boolean(true);
  2. public static final Boolean FALSE = new Boolean(false);
  3. public static Boolean valueOf(String s) {
  4. return parseBoolean(s) ? TRUE : FALSE;
  5. }
  1. public Boolean(boolean value) {
  2. this.value = value;
  3. }
  4. public Boolean(String s) {
  5. this(parseBoolean(s));
  6. }

不变类的重用

如果一个类是不变的,那么它往往就是可重用的。下面,以对String类的重用为例来说明一下:

  1. public final class String
  2. implements java.io.Serializable, Comparable<String>, CharSequence {
  3. /** The value is used for character storage. */
  4. private final char value[];
  5. /** Cache the hash code for the string */
  6. private int hash; // Default to 0
  7. public String(String original) {
  8. this.value = original.value;
  9. this.hash = original.hash;
  10. }
  11. ...
  12. }

1. 极端情况——完全不重用

  1. //Disapproved
  2. for (int i = 0; i < 10; i++) {
  3. String str = new String("content");
  4. ...
  5. }

String的源码可以看出,每次循环都会在程序运行时新建一个对相同字符数组的引用 和 相同的哈希码cache。这些循环中的初始化操作,无疑影响着性能的表现。

2.改进后——实现重用

  1. String str = "content";
  2. for (int i = 0; i < 10; i++) {
  3. ...
  4. }

改进后,不仅减少了重复初始化的性能损耗,而且String类的实例可以被在相同JVM上同时进行的其他程序共享,更加高效。

拓展阅读

关于String类的重用,和普通类相比实际上较为复杂,想要进一步了解可参考阅读这篇文章

可变类的重用

当已知可变类的实例不会被更改时重用实例

就像不变类的重用一样,如果你已经事先知道了某个可变类的实例在运行时并不会变化,那么这个实例就可以被重用。例如,Date类(可变类)的实例在其值被确定后就不再改变,那么就非常适合拿来重用。仔细观察下面的例子,你会发现虽然并未出现循环,但是某个频繁使用的方法中的内部变量也亟需重用,来减少那些容易被忽略的性能损耗。

  1. public class Person{
  2. private final Date birthDay;
  3. //Other fields, methods and constructors omitted
  4. //DON'T DO THIS
  5. public boolean isBabyBoomer(){
  6. //Umeccessary alloction of expensive object
  7. Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
  8. gmtCal.set(1946,Calendar.JANUARY,1,0,0,0);
  9. Date boomStart = gmtCal.getTime();
  10. gmtCal.set(1965,Calendar.JANUARY,1,0,0,0);
  11. Date boomEnd = gmtCal.getTime();
  12. return birthDate.compareTo(boomStart) >= 0 && birthDate.compareTo(boomEnd) < 0;
  13. }
  14. }

如下,改进后的Person类,只初始化了Calendar TimeZone Date类的实例一次,而不是在方法被调用时反复初始化。这样改进后,性能的提升是非常明显的(一千万次执行的耗时从32000ms下降到了130ms);而且将类的内部变量独立成为类的静态内部常量使逻辑更为清晰,可读性更强。

  1. public class Person {
  2. private final Date birthDate;
  3. // Other fields, methods, and constructor omitted
  4. /**
  5. * The starting and ending dates of the baby boom.
  6. */
  7. private static final Date BOOM_START;
  8. private static final Date BOOM_END;
  9. static {
  10. Calendar gmtCal =
  11. Calendar.getInstance(TimeZone.getTimeZone("GMT"));
  12. gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
  13. BOOM_START = gmtCal.getTime();
  14. gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
  15. BOOM_END = gmtCal.getTime();
  16. }
  17. public boolean isBabyBoomer() {
  18. return birthDate.compareTo(BOOM_START) >= 0 &&
  19. birthDate.compareTo(BOOM_END) < 0;
  20. }
  21. }

但是,上例的改进措施并不一定会获得巨大的性能提升,上例中的Calendar类的初始化十分耗时才导致了性能的巨大提升。

另外值得注意的一点就是,在Person类被使用时,BOOM_START BOOM_END字段的初始化为类的初始化带了额外的性能开销。为了减少额外的开销,可以将字段的加载延迟到实际使用时再进行,即:lazily initialize。实际的操作可以是将字段封闭在静态内部类中,然后再通过实现完全的延迟初始化。但是,为了实现完全的延迟初始化,不仅带来了复杂的实现结构,而且又会损耗性能,可能会丢掉已经获得的巨大性能提升,得不偿失。

保持敏锐的嗅觉

上面的例子很明显地需要提取重复的变量,从而实现重用,而还有一些情况并没有这么明显,需要编程人员保持敏锐的嗅觉。例如,适配器Adaptor设计模式(也被称为视图view设计模式)中我们就容易忽略其重用性。Adaptor是一个负责后台对象的、为后台对象提供可选接口的对象。一个Adaptor对象,除了它所代表的后台对象的状态,它本身是没有状态的。所以,创建多个Adaptor对象是没有必要的。

再比如,Map接口的keySet方法返回了一个包含了映射中所有键值的Set对象。看起来每次调用该方法,都会得到不同的Set对象。但实际上,对于一个给定的Map对象,多次调用该方法得到的是同一个Set实例。尽管Set实例是可变的,但是所有返回值却是一致的:一个返回的对象改变了,所有的返回对象都同时改变了,因为这个对象对应的是同一个Map对象。返回多个keySet视图对象(由Map产生,随它变化)是无害的,但也是没有必要的。

在Java 1.5中有了新的创建不必要对象的方式,叫做自动装(拆)箱(自动将原始类型装箱为包装类亦或反之)。自动装(拆)箱模糊了原始类型与包装类的界限,但并没有消除这个界限:还有一些细微的语义上的区别和不细微的性能上的差异。请看下面的程序,它将所有int类型的正数加了起来。为了不产生最高位的进位,程序使用了Long来保存结果。

  1. // Hideously slow program! Can you spot the object creation?
  2. public static void main(String[] args) {
  3. Long sum = 0L;
  4. for (long i = 0; i < Integer.MAX_VALUE; i++) {
  5. sum += i;
  6. }
  7. System.out.println(sum);
  8. }

程序给出了正确的答案,但是它远比它本来应该的速度慢。原因就在一个字母的错误。变量sum被声明为Long类型而非long类型,这就意味着这个程序创建了Long的实例。原程序的运行时间达到了43秒,而将sum更改为long类型后缩短到了6.8秒。这就告诉我们偏爱原始类型而少用包装类,并且小心潜在的拆装箱操作。

总结

本篇文章告诉你的不是:创建实例是“昂贵”(耗费资源)的、应该避免的。相反,一些构造方法做了很少的事情,这些小的对象的创建与回收是“廉价”的,尤其在现代的JVM上。创建额外的对象去加强程序的明确性、简洁性或者通常是不错的选择。

维持你自己的对象池的对象构建是应该避免的,除非对象池中的对象都是重量级 (构建时非常耗费资源或时间)的。经典的案例就是数据库的连接(例如:Tomcat中DBCP2),构建对象的成本足够高以至于重用是合理的。此外,数据库还可能限制了连接数的上限。大体上来讲,维持你自己的对象池会使你的代码杂乱不堪,增加了内存印记,还影响着性能。现代JVM有着高度优化的垃圾回收机制,在与轻量级的对象池的比拼上可以轻松胜出。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注