第7条:避免使用终结方法

第7条:避免使用终结方法

终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。使用终结方法会导致行为不稳定,降低性能以及可移植性问题。

C++程序员被告知“不要把终结方法当做C++中的析构器(destructors)的对应物”。在C++中,析构器是回收一个对象所占用资源的常规方法,是构造器所必需的对应物。在Java,当一个对象变得不可到达的时候,垃圾回收器会回收与该对象相关联的存储空间,并不需要程序员做专门的工作。C++的析构器也可以被用作回收其它的非内存资源。而在Java中,一般用try-finally块来完成类似的工作。

终结方法的缺点在于不能保证会及时地执行,从一个对象变得不可到达开始到它终结方法被执行,所花费的这段时间是任意长的,这就意味着,注重时间(time-critical)的任务不应该由终结方法来完成。及时地执行终结方法正是垃圾回收算法的一个主要功能,这种算法在不同的JVM上的执行时间是大相径庭的。

Java语言规范不仅不保证终结方法会被及时地执行,而且根本不保证它们会被执行。当一个程序终止的时候,某些已经无法访问的对象上的终结方法却根本没有被执行,这是完全有可能的。

结论:不应该依赖终结方法来更新重要的持久状态。例如,依赖终结方法来释放共享资源(比如数据库)上的永久锁,很容易让整个分布式系统垮掉。

当你不确定是否应该避免使用终结方法的时候,这里还有一种值得考虑的情形:如果未被捕获的异常在终结过程中被抛出来,那么这种异常可以被忽略,并且该对象的终结过程也会被终止。未被捕获的异常会使对象处于破坏的状态(a corrupt state),如果另一个线程企图用这种被破坏的对象,则可能发生任何不确定的行为。正常情况下,未被捕获的异常将会使线程终止,并打印出栈轨迹(Stack Trace),但是,如果异常发生在终结方法中,则不会如此,甚至连警告都不会打印出来。

还有一点:使用终结方法有一个非常严重的(Severe)性能损失。使用终结方法创建和销毁一个对象比使用普通方法创建和销毁对象慢大约400倍。

那么,如果类的对象中封装的资源(例如文件或者线程)的确需要终止,应该怎么做才能不编写终结方法呢?只需提供一个显示的终止方法,并要求该类的客户端在每一个实例不在有用的时候调用这个方法。值得一提的细节是,该实例必须记录下自己是否已经被终结了:显示的终止方法必须在一个私有域中记录下“该对象已经不在有效”。如果这些方法是在对象已经终止之后被调用,其它的方法就必须检查这个域,并抛出IllegalStateException异常。显示终结方法的典型例子是InputStream、OutputStream和java.sql.Connection上的close方法。

显示终结方法经常与try-finally结构结合起来使用,以确保及时终止。在finally子句内部用显示的终止方法,可以确保即使在使用对象的时候有异常抛出,该终止方法也会执行:

1 Foo foo = new Foo(...);
2 try{
3   //Do what must be done with foo
4   ...
5 }finally{
6   foo.terminate();        //Explicit termination method
7 }

使用终结方法的好处,它们有两种合法用途:

第一种用途是,当对象的所有者忘记调用前面建议的显示终止方法的时,终结方法可以充当“安全网(safety net)”。迟一点释放关键资源总比永远不释放要好。但是如果终结方法发现资源还未被终止,则应该在日志中记录一条警告,因为这是客户端的一个BUG,应当被修复。

第二种合理用途与对象的本地对等体(native peer)有关。本地对等体是一个本地对象(native object),普通对象通过本地方法(native method)委托给一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的Java对等体被回收的时候,它不会被回收。在本地对等体不拥有关键资源的前提下,终结方法正是执行这项任务最合适的工具。如果本地对等体拥有必须被及时终止的资源,那么该类就应该具有一个显示的终止方法,如前所述。终止方法应该完成所有必要的工作以便释放关键资源。终止方法可以是本地的,或者调用本地方法。

值得注意的很重要的一点是,“终结方法链(finalizer chaining)”并不会被自动执行。如果类(不是Object)有终结方法,并且子类覆盖了终结方法,子类的终结方法就必须手工调用超类的终结方法。你应该在一个try块中终结子类,并在相应的finally块中调用超类的终结方法。这样做可以保证:即使子类的终结过程抛出异常,超类的终结方法也会得到执行,反之亦然。代码示例如下,注意这个示例使用了Override注解(@Override):

1 //Manual finalizer chaining
2 @Override protected void finalizer() throws Throwable{
3     try {
4         ...     //Finalizer subclass state
5     }finally {
6         super.finalizer();
7     }
8 }

如果子类实现者覆盖了超类的终结方法,但是忘记了手工调用超类的终结方法(或者有意选择不调用超类的终结方法),那么超类的终结方法将永远不会被调用。要防范这样粗心大意或者恶意的子类是有可能的,代价就是为每个将被终结的对象创建一个附加的对象。不是把终结方法放在要求终结处理的类中,而是把终结方法放在一个匿名的类中,该匿名类的唯一作用就是终结它的外围实例(enclosing instance)。该匿名类的单个实例被称为终结方法守卫者(finalizer guardian),外围类的每个实例都会创建这样有个守卫者。外围实例在它的私有实例域中保存着一个对其终结方法守卫者的唯一引用,因此终结方法守卫者与外围实例可以同时启动终结过程。当守卫者被终结的时候,它执行外围实例所期望的终结行为,就好像它的终结方法是外围对象上的一个方法一样:

 1 //Finalizer Guardian idiom
 2 public class Foo {
 3     //Sole purpose of this object is to finalizer auter Foo object
 4     private final Object finalizerGuardian = new Object() {
 5         @Override protected void finalizer() throws Throwable {
 6             ...    //Finalizer outer Foo object
 7     
 8         }
 9     };
10     ...    //Remainder omitted
11 }

注意,公有类Foo并没有终结方法(除了它从Object中继承了一个无关紧要的之外),所以子类的终结方法是否调用super.finalize并不重要。对于每一个带有终结方法的非final公有类,都应该考虑使用这种方法。

总之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。在这用法些很少见的情况下,使用了终结方法,就要记住调用super.finalize。如果用终结方法作为安全网,要记得记录终结方法的非法用法。最后如果需要把终结方法与公有的非final类关联起来,请考虑使用终结方法守卫者,以确保即使子类的终结方法未能调用super.finalize,该终结方法也会被执行。

原文地址:https://www.cnblogs.com/remote/p/10092285.html