程序员

Effective.Java 读书笔记(5)复用对象

2017-02-19  本文已影响0人  Mezereon

5.Avoid creating unnecessary object

大意为 避免创建非必要的对象

通常来说我们每次重复使用一个对象是比重新创建一个功能上相等的对象更为合适的,复用可以更快并且更加优雅,当一个对象是不变的(Immutable)时候可以被经常重用

举一个极端的例子,考虑下列代码

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

这个语句执行的时候创建了一个String实例并且这个对象的创建是没有必要的,试想一下在一个循环或者频繁使用的方法里面这样用,会出现许多非必要的String实例

其实简单这样写就可以了

String s = "stringette";

这样写,我们只用了一个String实例而不是创建一个新的,而且,这保证了对象会被同一虚拟机中的其他任意的代码来复用

使用静态工厂方法你可以经常避免这种非必要的对象的创建,举个例子,静态的工厂方法,Boolean.valueOf(String)比起构造方法Boolean(String)是更加合适,构造方法每次会创建一个新的对象,然而静态工厂方法永远不会去这样做

对于复用不可变的对象,如果你知道哪些对象不会被修改你也可以复用那些可变的对象,这里略微有一点微妙,并且十分常见的例子关于不要去做的事,它调用了可变的Data对象,这个对象它的值一旦被计算出来后从未被更改,这个类对人建模,有着isBabyBoomer的方法,并且这个方法能告诉我们这个人是否是babyboomer,换句话说,就是这个人是否是出生在1946和1964年之间

public class Person {
  private final Date birthDate;
  // Other fields, methods, and constructor omitted
  // DON'T DO THIS!
  public boolean isBabyBoomer() {
  // Unnecessary allocation of expensive object
    Calendar gmtCal =
            Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
    Date boomStart = gmtCal.getTime();
    gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
    Date boomEnd = gmtCal.getTime();
    return birthDate.compareTo(boomStart) >= 0 &&
    birthDate.compareTo(boomEnd) < 0;
  }
}

这里的isBabyBoomer方法中,调用的时候创建了一个新的Calendar和TimeZone还有两个Date实例,这是没有必要的,下面利用静态的初始化类给出避免这种情况的代码,

class Person {
  private final Date birthDate;
  // Other fields, methods, and constructor omitted
  /**
  * The starting and ending dates of the baby boom.
  */
  private static final Date BOOM_START;
  private static final Date BOOM_END;
  static {
    Calendar gmtCal =
         Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
    BOOM_START = gmtCal.getTime();
    gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
    BOOM_END = gmtCal.getTime();
  }
  public boolean isBabyBoomer() {
      return birthDate.compareTo(BOOM_START) >= 0 &&
                birthDate.compareTo(BOOM_END) < 0;
  }
}

这个版本的Person类当它初始化的时候,创建Calendar,TimeZone和Date实例只有一次,而不是每次调用isBabyBoomer方法时都会创建新的,

这样的方法使得一些频繁调用的方法获得更加优越的表现,同时也使得代码更加清晰,增加了可读性

当然如果Calendar的实例的创建的代价十分重,通过这样的优化效果可能并不是很理想

如果Person类被初始化当时isBabyBoomer这个方法一直没有被调用的化那么BOOM_START和BOOM_END就会非必要地初始化了

消除这种非必要的初始化是有可能的,我们在isBabyBoomer方法第一次调用的时候可以使用懒初始化(lazily initializing)这些域,但这个并不推荐,经常使用懒加载会使得实现上更加复杂并且表现的提升可能没有我们所期望的那样

通过前面的例子我们知道当一个类在初始化之后不会被修改,那么我们很明显可以复用它,那么还有别的情况我们可以复用吗?当然了,我们的Adapter适配器就是一个例子,学习过Android的同学可能比较了解,ListView或者RecycleView就需要Adapter,一个Adapter在给定对于的对象的时候没有必要创建超过一个的实例

这里有种新的方式去创建一个非必要的对象,叫做autoboxing(自动封装),它允许程序员去混合原始的和封装后的原始类型,按照需求自动封装或者不封装,自动封装比较模糊但是不用去清除原始类型和封装后的原始类型的区别,他们有微妙的语义区别并且不那么微妙的表现差异,考虑一下下面计算正数的和的代码

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

使用Long纪录sum是因为int不够大来存放
这段代码很简单,可以得到正确的答案但是可能太慢了,原因是单字符排版错误,sum变量被声明成Long而不是long,这就意味着程序需要构造大概2^31个没有必要的Long实例,大概需要43秒,然后你改成long只需要6.8秒,机器差异不算的话,两者的表现实在相差太大了,我们更推荐使用原始类型而不是封装后的类型,小心不是我们所意的自动封装

这个我们在使用创建代价比较大的类的时候需要仔细注意,尽量避免,于此相反,创建一些小的对象,这些对象的构造方法十分轻松,特别是在当今JVM的实现上,创建额外对象去增强代码的清晰性,简单性或者程序的能力上绝对是好的

反过来,避免通过维持你自己的对象池(Object pool)来进行对象的创立,这是糟糕的想法除非你的对象在池中有着极端的权重,使用对象池的传统的例子是数据库的连接,建立连接的代价比较高,并且复用这些对象是有意义的,当然你的数据库证书可能会由于确定的连接的数目来限制你,总而言之,维持一个对象池会使你的代码杂乱,增加内存的开销并且降低表现,今天的JVM实现有着高度优化的垃圾收集器,它能够轻易地表现出更好的性能比起低权重对象的对象池

后话,重复创建不需要的对象仅仅影响风格和表现,如果涉及一些Defensive Copying防御性复制的话,那么安全漏洞和阴险的bug是更加重要的

上一篇下一篇

猜你喜欢

热点阅读