Java高效编程之一【创建和销毁对象】
一、考虑用静态工厂方法替代构造函数
代表实现:java.util.Collection Framework
Boolean类的简单例子:
public static Boolean valueOf (boolean b){ return(b ? Boolean.TRUE: Boolean.FALSE); }
优点:
1、与构造函数不同,静态工厂方法具有名字。
一个类看起来需要多个构造函数,并且它们的运行特征相同,应考虑使用静态工厂方法来替代其中一个或多个构造函数,并且要慎重选择它们的名字以明显标示他们的不同。
2、与构造函数不同,他们每次调用的时候,不要求非得创建一个新的对象。
当一个程序要频繁的创建相同的对象,并且创建对象的代价是昂贵的,这项技术可以极大地提高性能。
3、与构造函数不同,它们可以返回一个原返回类型的子类型的对象。
缺点:
1、类如果不含公有的或者受保护的构造函数,就不能被继承。
2、它们与其他的静态方法没有任何区别。
使用静态工厂方法要遵循命名习惯,其中两个已经很流行了
valueOf : 非常有效的类型转换符
getInstance : 返回实例,对于单例模式返回唯一的实例。
结论:构造函数和静态工厂要合理选择使用。
二、使用私有构造函数强化singleton属性
实现singleton有两种方法,这两种方法都要把构造函数保持为私有的,并且提供一个静态成员,以便能够允许客户访问该类的唯一实例
1、带有公有final域的方法
// Singleton with final field public class Elvis{ public static final Elvis INSTANCE=new Elvis(); private Elvis(){ …… } ……//Remained omitted }
2、静态工厂方法
// Singleton with static factory public class Elvis{ private static final Elvis INSTANCE=new Elvis(); private Elvis(){ …… } public static Elvis getInstance(){ return INSTANCE; } ……//Remained omitted }
如果确定永远是一个singleton,用第一种方法是有意义的,如果保留余地,让以后可以修改,使用第二种方法比价好。为了使一个singleton方法是可以序列化(serializable)的,仅仅在声明中加上"implements Serializable"是不够的。为了维护singleton性,必须加上一个readResolve方法,否则的话一个序列化的实例在反复序列化的时候,都会导致创建一个新的实例。为了防止这种情况在readResolve方法:
// readResolve 方法保持单例属性 private Object readResolve() throws ObjectStreamException{ /** *返回真正的Elvis,让垃圾收集器注意假冒的Elvis */ }
三、通过私有构造函数强化不可实例化的能力
将一个类包含单个显示的私有构造函数,则它就不可以被实例化了:
//不可实例化的实体类 public class UtilityClass { //不能实例化的抑制默认构造函数 private UtilityClass(){ //该构造函数将永远不能被调用 } ……//其余的省略 }
四、避免创建重复的对象
当你重用一已有对象的时候,请不要创建新的对象,而同样的,当你创建一个新的对象的时候,请不要重用一个已有对象。在提倡使用保护行拷贝的场合,因重用一个对象儿招致的代价要远远大于因创建重复对象而招致的代价。在要求保护性拷贝的情况下却没有实施保护性拷贝,将会导致错误和安全漏洞;而不必要的创建对象仅仅会影响程序的风格和性能。
五、消除过期的对象引用
考虑下面栈实现的例子,你能否发现内存泄露的位置:
public class Stack { private Object[] elements; private int size = 0; public Stack(int initialCapacity) { this.elements = new Object[initialCapacity]; } 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); } } }
从栈中弹出来的对象将不会当做垃圾回收,即使使用栈的客户程序不再引用这些对象,它们将不会回收。这是因为,栈内部维护这对这些对象的过期引用(过期引用,即永远也不会再被解除的引用),本例中凡是elements数组活动区域之外的引用都是过期的,elements的活动区域是指下标小于size的那一部分。
要想修复该问题很简单,一旦对象引用已经过期,只需要清空这些引用即可。pop方法的修订版如下:
public Object pop() { if (size==0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // 删除过期引用 return result; }
“清空度对象引用”这样的操作应该是一个例外,而不是一种规范行为。消除过期引用最好的办法是重用一个本来已经包含对象引用的变量,或者让这个 变量结束期声明周期。何时清除一个引用,Stack哪方面的特性使得它遭受到了内存泄露的影响?
根源在于Stack自己管理内存,存储池包含了elements数组(对象引用单元,而不是引用本身)的元素。数组活动区域中的元素是已分配的,而数组其余部分的元素是自由的。但垃圾回收器不知道,需要程序员手动清空。
六、避免使用终结函数
显示的终止方法通常与try-finally结构结合起来使用,以确保及时终止。
// try-finally 块保证终结方法的执行 Foo foo = new Foo(...); try { // 对fool执行一些必须操作 ... } finally { foo.terminate(); // 显示终结方法 }
另一种方法是“终结函数链(finalizeer chaining)”不会自动执行,如果一个类(不是Object类)有一个终结函数,并且只有一个子类改写了终结函数,那么子类的终结函数必须手工调用父类的终结函数。
// 手工终结链 protected void finalize() throws Throwable { try { // 终结子类的状态
... } finally { super.finalize(); } }
即使子类的终结过程中跑出一个异常,超类的终结函数也会被执行,反之亦然。
但是如果子类改写了超累的终结函数,但是忘记调用超累的终结函数(或者有意不调用),会使得超类的终结函数永远得不到执行。为了解决这一问题,我们使用匿名类的单个实例(即终结函数守卫者)来终结其外围实例。
// 终结函数守卫者 public class Foo { // 此对象的唯一目的是终结微微的Fool对象 private final Object finalizerGuardian = new Object() { protected void finalize() throws Throwable { // 终结外围的 Foo 对象 ... } }; // 匿名类
... // 余下省略 }
公有类Fool并没有终结函数(除了它从Object中继承了一个无关紧要的finalize之外),所以子类的终结函数是否调用super.finalize并不重要,对于每一个带有终结函数的公有非final类,都应该考虑使用这项技术。