《Effective Java》阅读笔记7 避免使用终结方法

304 阅读6分钟

1.序

本条的意思是,让你尽量不要在你的类中覆盖finalize方法,然后在在里面写一些释放你的类中资源的语句。

1.1为什么要避免覆盖并使用finalize方法?

  • (1)finalize方法不能保证它能被及时的执行。
  • (2)finalize方法甚至都不会被执行。
  • (3)System.gc和System.runFinalization这两个方法只是能增加finalize方法被调用的几率。
  • (4)唯一能保证finalize方法被执行的方法有两个,System.runFinalizersOnExit和Runtime.runFinalizersOnExit但是这两个方法已经被弃用。
  • (5)覆盖并使用终结方法会造成严重的性能损失。

1.2如果类中的资源确实需要被释放,我们应该怎么做?

一般来说,需要释放的资源有线程或者文件还有一下涉及到本地的资源的对象。

我们只需要提供一个public修饰的终止方法,用来释放资源,并要求这类的使用者在不再使用这个类的时候调用这个方法,并且在类中添加一个标志,来标记资源是否已经释放,如果已经被释放了,那这个类中的方法如果在被调用的话就抛出IllegalStateException异常,一个很好的例子就是InputStream和OutputStream。 多说一句,在调用我们自己定义的public修饰的终止方法的时候最好和try—finally一起使用,就像下面这样:

class MyObject{
    private boolean isClosed = false;
    //public修饰的终止方法
    public void close(){
        //资源释放操作
        ...
        isClosed = true;
    }
}
public static void main(String... args) {
    MyObject object = new MyObject();
    try{
        //在这里面使用object;
        ...
    }  finally {
        //在这里面关闭object;
        object.close();
    }
}

1.3析构函数和终结方法

学习过C++的同学看到终结方法(finalizer)应该马上就能想到C++中的析构函数(destructor)。

Java中的终结方法和C++中的析构函数类似,会在对象被垃圾回收之前执行,也就是对象被销毁之前执行。

在C++中,析构函数常用于回收对象所占用的资源。但由于GC机制的存在,在Java中,我们无法预知对象会在何时被回收,也就是说我们无法预知终结方法会在何时被执行,Java语言规范也不保证终结方法会被执行。这是十分危险的。

我们通过下面的例子演示一下析构函数和终结方法。

#include <iostream>using namespace std;
​
class A {
public:
    A(); // 构造函数,同Java
    ~A(); // 析构函数,删除对象时被执行
};
​
A::A() {
    cout << "创建对象" << endl;
}
​
A::~A() {
    cout << "删除对象" << endl;
}
​
​
int main() {
    A *a = new A();
    delete a; // 删除对象return 0;
}

创建对象
删除对象

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

2.终结方法的弊端

终结方法缺点一:

终结方法在于不能保证会被及时的执行。从一个对象变得不可达开始,到它的终结方法被执行,所花费时间是任意长的。这意味着,注重时间(time-critical)的任务不应该由终结方法来完成。

终结方法缺点二:

如果未被捕获的异常在终结过程中被抛出,那么这种异常可以被忽略,并且该对象的终结过程也会被终结。未被捕获的异常会使对象处于破坏的状态(a corrupt state),如果另一个线程企图使用该对象,则可能发生任何不确定的行为。 正常情况未捕获的异常会使线程终止并打印堆栈轨迹,但如果异常发生在终结方法中,甚至不会打印警告!!

终结方法缺点三:

使用了终结方法,会导致严重的性能损失。例如,在某个机器上创建并销毁一个简单对象时间约为5.6ns,增加一个终结方法则会增加到2400ns。

3.终结方法的好处

终结方法第一种合法用途:

当对象所有者忘记调用前面建议的显式终止方法时,终结方法可以充当 ==“安全网”(safety net)== 。

虽然这样做不能保证终结方法会被及时执行,但在客户端==无法通过显式调用终止方法==来正常结束操作的情况下,迟一点释放关键资源总永不释放要好如果终结方法发现资源仍未被终止,应该在日志中记录一条警告 )。 显式终止方法的实例(四个类:FileInputStream、FileOutputStream 、Connection 和 Timer)都具有终结方法,当终止方法不起作用,这些终结方法便当了安全网。

安全网”的作用是当我们提供的public修饰的终结方法被在外部忘记调用的时候提供一种安全保障,如下:

class MyObject{
    private boolean isClosed = false;
    //public修饰的终止方法
    public void close(){
        //资源释放操作
        ...
        isClosed = true;
    }

    //安全网
    @Overried
    protected void finalize() throws Throwable {
        try{
            close();
        }  finally  {
            super.finalize();
        }
    }}

终结方法的好处二

终结方法的第二种合理用途与对象的本地对等体(native peer)有关。本地对等体是一个本地对象(native object),普通对象通过本地方法委托给一个本地对象,因为本地对等体不是一个普通对象,所以垃圾回收期并不知道它。因此,在本地对等体并不拥有关键资源时,终结方法正是执行这项任务的最合适工具

如果本地对等体拥有必须被及时终止的资源,那么该类就应该具有一个显式的终止方法。这个终止方法就是完成必要的工作并释放关键资源。终止方法可以是本地方法或者它调用本地方法。

终结方法链”(finalizer)同样不会被自动执行,如果类有定义终结方法,并且子类覆盖了该终结方法,那么子类的终结方法就得手工调用父类的终结方法:以确保即使子类的终结方法过程抛出异常,父类的终结方法也会得以执行(这也是规避常见的代码攻击)。

// Manual finalizer chaining
  @Override
  protected void finalize() throws Throwable {
    try {
      // Finalize subclass state
    } finally {
      super.finalize();
    }
  }

另一种方式是使用终结方法守卫者(finalizer guardian)。我们不将终结方法封装在一个要求终结处理的类中,而是放在一个匿名类里,该匿名类唯一用途是终结它的外围实例(enclosing instance),该匿名类的单个实例就被称为终结方法守卫者。

如果子类实现者覆盖了超类的终结方法,但是忘了调用超类的终结方法,那么超类的终结方法永远不会调用。为了防止此种情况出现,可以使用终结方法守卫者,即为每个将被终结的对象创建一个附加的对象,该附加对象是一个匿名类实例,将外围类的终结操作如释放资源放入该匿名类的终结方法中。外围实例在它的私有实例域中保存着一个对其终结方法守卫者的唯一引用,因此终结方法守卫者与外围实例可以同时启动终结过程。当守卫者被终结时,它执行外围实例所期望的终结行为,就好像它的终结方法是外围对象上的一个方法一样

public class A {

    // 终结守卫者
    private final Object finalizerGuardian = new Object() {

        @Override
        // 终结守卫者的终结方法将被执行
        protected void finalize() {
            System.out.println("A finalize by the finalizerGuardian");
        }
    };


    @Override
    // 由于终结方法被子类覆盖,该终结方法并不会被执行
    protected void finalize() {
        System.out.println("A finalize by the finalize method");
    }


    public static void main(String[] args) throws Exception {
        B b = new B();
        b = null;
        System.gc();
        Thread.sleep(500);
    }
}

class B extends A {

    @Override
    public void finalize() {
        System.out.println("B finalize by the finalize method");
    }

}

结果:

1 A finalize by the finalizerGuardian
2 B finalize by the finalize method

4.总结

总而言之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用终结方法。

  • 1.在很少见的情况下,既然使用了终结方法,就要记住使用super.finalize。
  • 2.如果用作安全网,要记得记录终结方法的非法用法。
  • 3.如果需要将终结方法与公有的非final类关联起来,请考虑使用终结方法守卫者以确保:即使子类的终结方法没有调用super.finalize,该终结方法也会被执行。

5.参考文献

www.jianshu.com/p/bc96028e0… my.oschina.net/u/4689327/b…

关注公众号“程序员面试之道”

回复“面试”获取面试一整套大礼包!!!

本公众号分享自己从程序员小白到经历春招秋招斩获10几个offer的面试笔试经验,其中包括【Java】、【操作系统】、【计算机网络】、【设计模式】、【数据结构与算法】、【大厂面经】、【数据库】期待你加入!!!

1.计算机网络----三次握手四次挥手

2.梦想成真-----项目自我介绍

3.你们要的设计模式来了

4.震惊!来看《这份程序员面试手册》!!!

5.一字一句教你面试“个人简介”

6.接近30场面试分享

7.你们要的免费书来了