Effective Java读书笔记六

来源:互联网 发布:陕西旅游实时数据 编辑:程序博客网 时间:2024/05/16 09:31

         

Item 7:尽量不要使用finalizer

Joshua一上来就强调:Finalizer是无法预言的,危险的并且大部分情况下是不必要的。

使用finalizer会给代码带来不稳定的行为,低下的性能,还有可移植性的问题。

根据JLS,finalizer无法保证被按时的执行。我们知道,当一个对象过期之后,在垃圾回收器回收之前会自动的调用finalizer方法,所以有的时候我们就会把一些对象的收尾工作放在finalizer方法中,比如文件的关闭,数据库连接的释放等。但是,正是因为JLS的这个“无法保证”,会让程序不定期的崩溃。因此,即使需要使用finalizer,也不能把与执行顺序相关的代码放进去,否则程序就会出现不可预期的行为。
实际上,finalizer的执行时间,因不同JVM的算法而异。所以如果你的代码依赖了finalizer的执行时间,那么你的代码就有可能在某些JVM上是正确的,而在其它的JVM上就不能正常执行。

除了执行时间的不确定性,使用了finalizer方法还可能在某种情况下推迟对象的回收,如果某个庞大的对象的回收被推迟的话,就有可能引起OutOfMemoryError的错误。

JLS不但不能保证finalizer被按时执行,某些时候甚至无法保证它会被执行。比如当程序退出的时候,某些过期对象的finalizer可能不会被执行。所以Joshua建议,对某些重要的持久化状态的更新千万不要依赖finalizer。

不要自作聪明去调用System.gc或者System.runFinalization方法,这些方法也无法保证finalizer的执行。

当finalizer执行的时候,如果不幸抛出了异常,那么这个异常会被忽略,而且这个对象的回收也会终止,这样就会留下一个奇怪的对象,如果程序的其它部分“有幸”用到了这个奇怪的对象,那么这个程序也就奇怪了。

finalizer方法还有性能方面的问题。在作者的机器上,创建和消除一个简单的对象,没有finalizer的版本用了5.6ns,而增加了finalizer的版本用了2400ns,两者相差了430倍。所以对性能有要求的程序还是考虑一下吧。

说了这么多finalizer的问题,那么当我们真的需要做一些“收尾工作”的时候该怎么办呢?Joshua建议显式提供一个终结方法,并且配合try和finally来使用:

 

当然,如果要使用这种方法的话,那么提供了显式终结方法的对象应该还有一个状态来标记它是否已经被终结了,从而避免已经被终结的对象被误用。我们平时使用的许多类都有这个功能,比如FileInputStream,Connection,Graphics.dispose等等。

finalizer在两个方面是有用处的。第一,finalizer可以作为一个安全层来使用。当我们的类提供了显式终结方法时,为了防止显式终结方法没有被调用,我们可以在finalizer里来调用它,毕竟无法保证按时被调用总比从来不调用要强,因为有些资源还是需要释放的。不过,在这种情况下,最好在finalizer里记录一些log,可以提醒我们没有正确的调用终结方法。

第二个用到finalizer的地方是处理native方法和native对象的时候。因为native的对象和方法是不受JVM控制的,因此何时该回收native对象就只有调用它的那个对象才能知道,这个时候finalizer就可以用来回收native对象。当然如果native对象持有的是非常重要的资源的话,还是需要提供一个显式的终结方法来回收它们。

需要注意finalizer不像构造函数那样会自动向上调用,也就是说子类的finalizer不会自动的调用父类的finalizer。如果在需要的时候,可以将子类的代码放到try中,而将调用父类的finalizer放到finally中,这样就会在任何一个finalizer出现异常的时候还能保证另外一个执行:

 

最后是finalizer的一个比较巧妙的改进方案:在类A中创建一个private final的Object字段,引用一个匿名类,在匿名类中重写finalizer来回收A的资源: