本章内容:
1. 考虑用静态工厂方法代替构造器
2. 遇到多个构造器参数时要考虑用构建器(Builder模式)
3. 用私有构造器或者枚举类型强化Singleton属性
4. 通过私有构造器强化不可实例化的能力
5. 避免创建不必要的对象
6. 消除过期的对象引用
7. 避免使用终结方法
1. 考虑用静态工厂方法代替构造器
类可以通过静态工厂方法来提供它的客户端,而不是通过构造器。示例如下:
public class Gender{
private String description;
private Gender(String description){this.description=description;}
private static final Gender female=new Gender("女");
private static final Gender male=new Gender("男");
public static Gender getFemale(){
return female;
}
public static Gender getMale(){
return male;
}
public String getDescription(){return description;}
}
优点:
(1)静态方法有自己的名字,更加易于理解。
(2)不必在每次调用它们的时候都创建一个新对象。
(3)它可以返回原返回类型的的任何子类型的对象,如返回的父类的私有子类来隐藏实现类、而且返回的对象还可以随着参数的变化而变化(只要是返回类型的子类型)、而且在编写该静态工厂方法时返回类型的子类可以不必存在(如服务提供者框架)。
(4)在创建参数化类型实例的时候代码变得更加简洁
缺点:
(1)静态工厂方法返回类型如果不含有公有的或者保护的构造器,那该类就不能子类化(有子类,被继承),子类的构造函数需要首先调用父 类的构造函数,因为父类的构造函数是private的,所以即使我们假设继承成功的话,那么子类也根本没有权限去调用父类的私有构造函数,所以是无法被继承的 。
(2)它们与其他的静态方法实际上没有任何区别。所以我们要遵守标准和命名习惯:
valueOf--类型转换方法,该方法返回的实例与它的参数具有同样的值
of--valueOf的一种更加简洁的替代
getInstance--返回的实例是通过方法的参数来描述的,对于Singleton来说没有参数
newInstance--像getInstance一样,但newInstance能够确保返回的每个实例都与其他实例不同
getType、newType--Type表示返回的对象类型
2. 遇到多个构造器参数时要考虑用构建器(Builder模式)
静态工厂类和构造器有个共同的局限性,它们都不能很好地扩展到大量的可选参数。解决方法:
(1)重叠构造器模式,为每个可选参数增加一个构造器。这种方法代码难以阅读,而且设置参数时很容易出错。
(2)JavaBeans模式,用一个无参构造器来创建对象,然后调用setter方法来设置每个需要的参数。但是这种方法在构造过程中被分到几个调用中,在构造过程中JavaBean可能处于不一致的状态。JavaBean模式阻止了把类做成不可变的可能,这就需要程序员付出额外的努力来确保它的线程安全。
当对象的构造完成,并且不允许在解冻之前使用时,通过手工冻结对象,可以弥补这些不足,但是无法确保程序员会在使用之前先在对象上调用freeze方法。
(3)Builder模式,既能保证像重叠构造器模式那样的安全性,也能保证像JavaBeans模式那么好的友好性。示例如下:
Public class A{
private final int a;
private final int b;
public static class Builder{
private final int a;
private final int b;
// 必选参数
public Builder(int a){
this.a=a;
}
public Builder b(int v){
b = v;
return this;
}
public A build(){
return new A(this);
}
}
private A(Builder builder){
a = builder.a;
b = builder.b;
}
}
//使用
A o = new A.Builder(10000).b(50000).build();
Builder模式的确也有它自身的不足,为了创建对象,必须先创建它的构建器。虽然构建器的开销在实践中可能不那么明显,但是在某些十分注重性能的情况下,可能就成问题了。
3. 用私有构造器或者枚举类型强化Singleton属性
Singleton指仅仅被实例化一次的类,用来代表那些本质上唯一的系统组件,比如窗口管理器或者文件系统。
在Java1.5之前,实现Singleton有以下两种方法:
方法一:
public class A{
public static final A a = new A();
private A(){...};
...
}
公有静态成员是个final域,私有构造器仅被调用一次,用来实例化公有的静态final域a。由于缺少公有的或者受保护的构造器,保证A只存在一个实例。但要注意一点,享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器,如果要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。
方法二:
public class A{
private static final A a = new A();
private A(){...};
public static A getInstance(){ return a; };
...
}
公有的成员是个静态工厂方法,对于静态方法A.getInstance的所有调用,都会返回同一个对象引用,所有也只有一个实例。注意同样存在上面通过反射机制调用私有构造器的问题。
为了使上面两种方法实现的Singleton类变成可序列化的,仅仅在声明中加上impliments Serializable是不够的。为了维护并保证Singleton,必须声明所有实例域都是瞬时(transient)的,交提供一个readResolre方法,否则每次反序列化一个序列化的实例时,都会创建一个新的实例。
private Object readResolve(){
...
return a;
}
在Java1.5起,实现Singleton有了第三种方法
方法三:
public enum A{
INSTANCE;
}
这种方法只需编写一个包含单个元素的枚举类型,在功能上与公有域方法相近,但是它更加简洁,无偿地提供序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候。单元素的枚举类型已经成为实现Singleton的最佳方法。
4. 通过私有构造器强化不可实例化的能力
有些工具类(utility class)不希望被实例化,实例化对它没有任何意义。然而在缺少显式构造器的情况下,编译器会自动提供一个公有的、无参的缺少构造器,对用户而言,这个构造器与其他的构造器没有任何区别。
企图通过将类做成抽象类来强制该类不可被实例化也是行不通的,该类可以被子类化,并且该子类也可以被实例化。这样做甚至会误导用户,以为这种类是专门为了继承而设计的。
由于只有当类不包含显式的构造器时,编译器才会生成缺少的构造器,所以:
public class A{
private A(){
throw new AssertionError(); //不是必须,但可以避免不小心在类的内部调用构造器
}
...
}
这种用法也有副作用,它使得一个类不能被子类化。
5. 避免创建不必要的对象
一般来说,最好能重用对象而不是在每次需要的时候就创建一个相同功能的新对象。如果对象是不可变的,它就始终可以被重用。
String s = new String("111"); //该语句每次被执行的时候都创建一个新的String实例,但是这些创建对象的动作全都是不必要的。
String s = "111"; //这个版本只存在一个String实例,而不是每次执行的时候都创建一个新的实例。而且对于所有在同一台虚拟机中运行的代码,只要它们包含相同的字符串字面常量,该对象就会被重用。
除了重用不可变的对象之外,也可以重用那些已知不会被修改的可变对象,如下:
public class A{
private final Data birthData;
public boolean is BabyBoomer(){
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Data start = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Data end = gmtCal.getTime();
return birthData.compareTo(start) >=0 && birthDate.compareTo(end) < 0;
}
}
上面方法每次调用都会创建一个Calendar、一个TimeZone和两个Date实例。改进如下:
public class A{
private final Data birthData;
private final Data start;
private final Data emd;
static{
Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0);
Data start = gmtCal.getTime();
gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0);
Data end = gmtCal.getTime();
}
public boolean is BabyBoomer(){
return birthData.compareTo(start) >=0 && birthDate.compareTo(end) < 0;
}
}
改进后的A类只在初始化的时候创建Calendar、TimeZone、Date实例一次,而不是每次都去创建这些实例(速度大给快了250倍)。而且代码更容易让人理解。
要优先使用基本类型而不是装箱基本类型,防止无意识的自动装箱创建多余的对象。
不要错误的认为“创建对象的代价非常昂贵,我们应该要尽可能地避免创建对象”。相反,由于小对象的构造器只做很少量的显式工作,它的创建和回收是非常廉价的,特别是在现代的JVM实现上更是如此。可以通过创建附加的对象来提升程序的清晰性、简洁性和功能性。反之,通过维护自己的对象池来避免创建对象并不是一种好的做法,除非池中的对象是非常重量级的,维护自己的对象池必定会把代码弄得很乱,同时增加内存占用,并且还会损害性能。现代的JVM实现且有调度优化的垃圾回收器,其性能很容易就会超过轻量级对象池的性能。
6. 消除过期的对象引用
在支持垃圾回收的语言中,内存泄漏是很隐藏的(称这类内存泄漏为无意识的对象保持)。如果一个对象引用被无意识地保留起来了,那么垃圾回收机制不仅不会处理这个对象,而且也不会处理被这个对象所引用的所有其他对象。从而对性能造成潜在的重大影响。
这类问题的修复方法很简单:一旦对象引用已经过期,只需清空这些引用即可(o = null;)。清空过期引用的另一个好处是,如果它们以后又被错误地解除引用,程序就会立即抛出NullPointerException异常,而不是悄悄地错误运行下去。
当程序员第一次被类似这样的问题困扰的时候,他们往往会过分小心:对于每一个对象引用,一旦程序不再用到它,就把它清空。其实这样做既没必要,也不是我们所期望的,因为这样做会把程序代码弄得很乱。清空对象引用应该是一种例外,而不是一种规范行为。
一般而言,只要类是自己管理内存,程序员就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。
内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉。对于这个问题有几种可能的解决方案:只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。
更为常见的情形则是,“缓存项的生命周期是否有意义”并不是很容易确定,随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该时不时地清除掉没用的项。这项清除工作可以由一个后台线程(可能是Timer或者ScheduleThreadPoolExecutor)来完成,或者也可以给缓存添加新条目的时候顺便进行清理。
内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,它们就会积聚。确保回调立即被当作垃圾回收的最佳方法是只保存它们的弱引用。
借助Heap剖析工具(Heap Profiler)检测内存泄漏问题。
7. 避免使用终结方法
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。 在Java中,一般用try-finally块来完成类似的工作。
终结方法的缺点在于不能保证会被及时地执行。从一个对象变得不可到达开始,到它的终结方法被执行,所花费的这段时间是任意长的。由于JVM会延迟执行终结方法,所以大量的文件会保留在打开状态,当一个程序再不能打开文件的时候,它可能会运行失败。如果程序依赖于终结方法被执行的时间点,那么这个程序的行为在不同的JVM实现中会大相径庭。 终结方法线程的优先级比该应用程序的其他线程的要低得多,如果终结速度达不到它们进入队列的速度就会出现OutOfMemoryError。
Java语言规范不仅不保证终结方法会被及时的执行,而且根本不保证他们会被执行。当一个程序终止的时候,某些已经无法访问的对象上的终结方法却根本没有被执行。所以不应该依赖终结方法来更新重要的持久状态。
不要被System.gc和System.runFinalization这两个方法所诱惑,他们确实增加了终结方法被执行的机会,但是他们不保证终结方法一定被执行。唯一声称保证终结方法被执行的方法是System.runFinalizersOnExit,以及他臭名昭著的孪生兄弟Runtime.runFinalizersOnExit。这两个方法都有致命的缺陷,都被废弃了。
如果未被被捕获的异常在终结过程中被抛出来,那么这种异常可以被忽略,并且该对象的终结过程也会终止。正常情况下,未被捕获的异常将会使线程终止,并打印出栈轨迹,但是如果异常发生在终结方法之中,则不会如此,甚至连警告都不会打印出来。
使用终结方法有一个非常严重的(Severe)性能损失。用终结方法创建和销毁对象慢了大约430倍。
正确的终结方法为:提供一个显示的终止方法,并要求该类的客户端在每个实例不再有用的时候调用这个方法。该实例必须记录下自己是否已经被终止了,显式的终止方法必须在一个私有域中记录下“该对象已经不再有效”。如果这些方法是在对象已经终止之后被调用,其他的方法就必须检查这个域,并抛出IllegalStateException异常。
显示终止方法的典型例子是InputStream、OutputStream和java.sql.Connection上的close方法。另一个例子是java.util.Timer上的cancel方法,它执行必要的状态改变,使得与Timer实例相关联的该线程温和的终止自己。java.awt中的例子还包括Graphics.dispose和Window.dispose。Image.flush,他会释放所有与Image实例相关联的资源,但是该实例仍然处于可用的状态,如果有必要的话,会重新分配资源。
显式的终止方法通常与try-finally结构结合起来使用,终结方法可以充当“安全网”,迟一点释放关键资源总比永远不释放要好,如果终结方法发现资源还未被终止则应该在日志中记录一条警告,应及时修复。
“终结方法链”并不会被自动执行,如果类有终结方法,并且子类覆盖了终结方法,子类的终结方法就必须手工调用超类的终结方法。
总结:除非作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。在这些很少见的情况下,既然使用了终结方法,就要记住调用super.finalize。如果用终结方法作为安全网,要记得记录终结方法的非法用法。最后,如果需要吧终结方法与公有的非final类关联起来,请考虑使用终结方法守护者,以确保即使子类的终结方法未能调用super.finalize,该终结方法也会被执行。