• 问题

    一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的实例,如果对象是不可变的,那么它就始终可以被重用。作为一个反例,代码如下

    String s = new String("test");
    

    如果这条语句出现在循环中就会在每一次循环时生成一个新的String实例,而这些String实例的功能完成一样,这会造成资源的巨大浪费,改进版本如下

    String s = "test";
    

    这种写法只用了一个String实例,而不是每次都创建一个新的实例,而且,它可以保证在所有同一台虚拟机上运行的代码,只要它们包含的字符串字面量相同,就可以保证该对象会被重用。

    那么,在使用对象避免反复创建对象有哪些原则?

  • 解决

    为了避免重复对象,在实际编码过程中可以从这几个方面来思考:

    1. 对于同时提供了静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,原因在于构造器在每次被调用的时候都会创造一个新的对象,而静态工厂方法则不要求这样做,完成可以返回同一个对象的引用。例如静态工厂方法Boolean.valueOf(String)总是优先于Boolean(String);

    2. 除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象。例如下面的例子:

      public class Person {
          private final Date birthDate;
      
          // Other fields, methods, and constructor ommitted
          // 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实例,这是不必要的,因这它们一旦被创建即无需改变了。下面是一个用静态的初始化器的版本,避免了这种效率的情况。

      public class Person {
          private final Date birthDate;
          // Other fields, methods, and constructor ommitted
      
          /**
           * 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);
              Date boomStart = gmtCal.getTime();
              gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
              Date boomEnd = gmtCal.getTime();
          }
      
          public boolean isBabyBoomer() {
              return birthDate.compareTo(BOOM_START)>=0 && birthDate.compareTo(BOOM_END)<0;
          }
      }
      

      还有一个问题,就是如果改进后Person类被初始化了,它的isBabyBoomer方法永远不会被调用,那么就没有必要初始化两个静态域了。通过延迟初始化则有可能消除这些不必要的初始化方法,但是不建议这样做。这样做使方法的实现变的更加复杂,从而无法将性能显著提高到超过已经达到的水平;

    3. 警惕自动装箱和拆箱。在自动装箱(autoboxing)的机制下,可以将基本类型和装箱基本类型混用,但是这两种类型在性能上有着比较明显的差别,如反例:

      public static void main(String[] args) {
          Long sum = 0L;
          for (long i = 0; i < Integer.MAX_VALUE; i++) {
              sum += i'
          }
          System.out.println(sum);
      }
      

      问题在于sum被声名成Long而不是long,Long是装箱基本类型,而long是基本类型,这样就在无意的情况了增加了拆装箱的开销,结论是:要优先使用基本类型而不是装箱基本类型,要当心无意识的自动装箱

  • 结论

    为了避免创建对象,而重用对象是为了提升性能,但是,这也应该有一个”度“。比如小对象的构造器只做很少量的工作,所以,小对象的创建和回收是非常廉价的,特别是在现在的JVM上,此时,就不需要刻意的为了重用对象而让代码逻辑更加混乱。反之,维护自己的对象池来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的。比如说数据库连接池,建立数据库连接的代价非常昂贵,因为重用这些对象很有意义。

results matching ""

    No results matching ""