Effective java笔记2--创建于销毁对象

时间:2022-03-12 13:08:45

一、创建对象的两种方式

1、提供公有的构造器。

2、提供一个返回类实例的静态方法。

二、使用静态方法创建对象

优势:

1、静态工厂方法的一个好处是,与构造函数不同,静态工厂方法具有名字。产生的客户端代码更易于阅读。

//例如,构造函数BigInteger(int,int,Random)返回的BigInteger可能是素数,
BigInteger.probalePrime()的静态工厂方法,表达显然更为清楚

2、静态工厂方法的第二个好处,与构造函数不同,它们每次被调用的时候,不要求非得创建一个新的对象。对于不可变类,可以预先使用构建好的实例,或将构建好的实例缓存起来,进行重复利用,从而避免创建不必要的重复对象。

3、静态方法可以返回原返回类型的任何子类型的对象,这对于基于接口的框架提供了更大的灵活性。

4、静态方法在创建参数化类型实例的时候,可以使代码变得更加简洁。

缺点:

1、类如果不含公有的或受保护的构造器,就不能被子类化。有时这正好是优势,复合比继承更高效。

2、用于创建对象静态方法的静态方法实际上与其他静态方法没有任何区别,所以除非文档有详细的描述,否则很难找到如何实例化通过这种方法创建的类对象。

最佳实践:

1、当一个类需要多个带有相同签名的构造器时,就用静态工厂方法代替构造器,并且慎重地选择名称以便突出它们之间的区别。

2、要求客户端通过接口来引用被返回的对象,而不是通过它的实现类,即返回类型为接口而不是实现类。

3、使用惯用名称来命名静态工厂方法:

valueOf:不太严格的讲,该方法返回的实例与它的参数具有相同的值,这样的静态工厂方法实际上是类型转换方法。

of:valueOf的一种更为简洁的替代,在EnumSet中使用并流行起来。

getInstance:返回的实例是通过方法的参数来描述的,但是不能够说与参数具有同样的值。对于Singleton来说,该方法没有参数,且返回唯一的实例。

newInstance:像getInstance一样,但newInstance能够确保返回的每个实例都与其他所有实例不同。

getType:像getInstance一样,但在工厂方法处于不同的类中时使用。Type表示工厂方法所返回的对象类型。

newType:像newInstance一样,但在工厂方法处于不同的类中时使用。Type表示工厂方法所返回的对象类型。

三、避免创建重复的对象

重复使用同一个对象,而不是每次需要的时候就创建一个功能上等价的新对象,通常前者更为合适。重用方法既快速,也更为流行。如果一个对象时非可变的(immutable),那么它总是可以被重用。

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

上面的实参"silly"本身就是一个String实例,功能上等同于所有被构造函数创建的对象,如果这种用法放在一个循环或者一个被频繁调用的方法中,那么成千上万的String实例会被创建出来。

改进版本如下:

String s = "No longer silly";

这个版本只是用一个String实例,可以保证对于所有在同一个虚拟机中运行的代码,只要他们包含相同的字符串字面常量,则该对象就会被重用。

不要错误地认为本条目所介绍的内容暗示”创建对象的代价是非常昂贵的,我们应该要尽可能地避免创建对象“。相反,由于小对象的构造函数只做很少量的工作,所以,小对象的创建和回收动作非常廉价的,特别是在现代的JVM实现上更是如此,通过创建附加的对象,以使得一个程序更加清晰、简洁、功能强大,这往往也是一件好事。

反之,通过维护自己的对象池(object pool)来避免对象的创建工作并不是一个号的做法,除非池中的对象时非常重量级的。一个正确使用对象池的典型例子就是数据库连接池。建立数据库连接的代价非常昂贵的,因此重用这样的对象非常有意义。

四、清除过期的对象引用

由于java是支持垃圾回收的语言,内存泄露是很隐蔽的,举个例子:

//Can you spot the "memory leak"?
public class Stack{
     private Object[] elements;
     private int size = 0;

     public Stack(int initialCapacity){
          this.elements = new Object[initalCapacity]; 
    }  

     public void push(Object e){
          ensureCapacity();
          elements[size++] = e;
     }

     public Object pop(){
          if(size==0)
                throw new EmptyStackException();
          return elements[--size];
     }

      private void ensureCapacity(){
          if(elements.length==size){
               Object[] oldElements = elements;
               elements = new Object[2*elements.length+1];
               System.arraycopy(oldElements,0,elements,0,size);

          }
     }
}

上面的程序中潜伏着一个问题,不严格地讲,这个程序有一个”内存泄露“,随着垃圾回收器活动的增加,或者出于不断增加的内存占用,程序性能的降低会逐渐表现出来。如果栈先是增长,然后再收缩,那么,从栈弹出的对象将不会被当垃圾回收,即使使用栈的客户程序不再引用这些对象,它们也不会被回收。这是因为,栈内部维护着对这些对象的过期引用(absolete reference)。所谓过期引用,是指永远也不会被解除的引用。在本例中,凡是在elements数组的”活动区域(active protion“之外的引用都是过期的,elements的活动区域是指下标小于size的那一部分。

如果一个对象引用被无意识地保留起来了,那么,垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象引用的所有其他对象,即使只有少量的几个对象引用被无意识地保存下来,也会有很多的对象被排除在垃圾回收机制之外,从而对性能造成潜在的重大影响。

对本案例修复方法:

pubic Object pop(){
     if(size==0) throw new EmptyStackException();
     Object result = elements[--size];  
     elements[size] = null; //Eliminate obsolete reference
     return result;  
}

清除过期引用的另外一个好处是,如果他们在以后被错误地解除引用,则程序会立即抛出NullPointerException异常,而不是悄悄地错误地运行下去。尽可能早的检测出程序中的错误总是有益的。