java:effectivejava学习笔记之避免使用终结方法(代码片段)

JMW1407 JMW1407     2023-02-18     607

关键词:

避免使用终结方法

1、finalize()基本概念

学习过C++的同学看到终结方法(finalizer)应该马上就能想到C++中的析构函数(destructor)。Java中的终结方法果然和C++中的析构函数类似,会在对象被垃圾回收之前执行,也就是对象被销毁之前执行。在C++中,析构函数常用于回收对象所占用的资源。但由于GC机制的存在,在Java中,我们无法预知对象会在何时被回收,也就是说我们无法预知终结方法会在何时被执行,Java语言规范也不保证终结方法会被执行。这是十分危险的。

所谓的终结方法其实是指finalize()。

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

#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;

输出

创建对象
删除对象

Java

public class A 
    @Override
    protected void finalize() throws Throwable 
        System.out.println("A调用了终结方法...");
    


public class B 
    @Override
    protected void finalize() throws Throwable 
        System.out.println("B调用了终结方法...");
    


public class Main 
    public static void main(String[] args) 
        WeakReference<A> weakReference = new WeakReference<>(new A());
        B b = new B();
        System.gc(); // 弱引用在GC时,会被回收
    

输出

A调用了终结方法...

在Java9以后,终结方法就已经被抛弃了。如果你尝试重写这个方法,会发现它是过时的。

Java9用清除方法( cleaner)替代了终结方法,但是由于清除方法的执行时间也是不确定的、也不保证会被执行,所以同样不推荐使用。

永远不要用终结方法或者清除方法来释放资源。

2、finalize()的执行过程

当对象不可达时,GC会判断该对象是否重写了finalize()方法,如没有重写则直接将其回收,否则,若对象未执行过finalize()方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize()方法。执行finalize()方法完后,GC会再次判断该对象是否可达,若不可达则进行回收。否则对象“复活”。

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

(1)finalize方法不能保证它能被及时的执行。

(2)finalize方法甚至都不会被执行。

(3)System.gcSystem.runFinalization这两个方法只是能增加finalize方法被调用的几率。

(4)唯一能保证finalize方法被执行的方法有两个,
System.runFinalizersOnExitRuntime.runFinalizersOnExit但是这两个方法已经被弃用。

(5)覆盖并使用终结方法会造成严重的性能损失。

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

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

  • 我们只需要提供一个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();
    

5、终结方法的利弊

5.1、终结方法的好处

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

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

虽然这样做不能保证终结方法会被及时执行,但在客户端无法通过显式调用终止方法来正常结束操作的情况下,迟一点释放关键资源总永不释放要好(如果终结方法发现资源仍未被终止,应该在日志中记录一条警告 )。

显式终止方法的实例(四个类:FileInputStreamFileOutputStreamConnectionTimer)都具有终结方法,当终止方法不起作用,这些终结方法便当了安全网。

“安全网”的作用是当我们提供的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

5.2、终结方法的弊端

终结方法缺点一:

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

终结方法缺点二:

如果未被捕获的异常在终结过程中被抛出,那么这种异常可以被忽略,并且该对象的终结过程也会被终结。未被捕获的异常会使对象处于破坏的状态(a corrupt state),如果另一个线程企图使用该对象,则可能发生任何不确定的行为。

正常情况未捕获的异常会使线程终止并打印堆栈轨迹,但如果异常发生在终结方法中,甚至不会打印警告!!

终结方法缺点三:

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

6、总结

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

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

参考

1、java代码优化——避免使用终结方法
2、《Effective Java》阅读笔记7 避免使用终结方法

java:effectivejava学习笔记之始终要覆盖tostring方法(代码片段)

Java始终要覆盖toString方法始终要覆盖toString方法1、为什么要覆盖toString方法?2、始终要覆盖toString方法始终要覆盖toString方法1、为什么要覆盖toString方法?publicclassPerson protectedStringname; protectedintage; publicStringgetName() retur... 查看详情

java:effectivejava学习笔记之优先考虑泛型和泛型方法(代码片段)

Java优先考虑泛型和泛型方法1、优先考虑泛型2、优先考虑泛型方法参考1、优先考虑泛型下面我们举个例子,将他作为泛型化的主要备选对象,换句话说,可以适当的强化这个类来利用泛型。publicclassStackprivateObject[]elem... 查看详情

effectivejava学习笔记之创建和销毁对象

一、考虑用静态工厂方法代替构造器1、此处的静态工厂方法是指返回指为类的对象的静态方法,而不是设计模式中的静态工厂方法。2、静态工厂方法的优势有:a、使用不同的方法名称可显著地表明两个静态工厂方法的不同,而... 查看详情

effectivejava学习笔记之所有对象都通用的方法

一、覆盖equals时请遵守通用约定1、满足下列任何一个条件时,不需要覆盖equals方法a、类的每个实例本质上都是唯一的。此时就是Object中equals方法所表达的含义。b、不关心类是否提供了“逻辑相等”的测试功能c、超类中覆... 查看详情

effectivejava学习笔记之不可实例化的类

在没有显式声明一个类的构造方法时,编译器会生成默认的无参构造方法,在设计工具类时,我们通常将方法设置成静态方法,以类名.方法名的形式调用,此时这个类就没有必要创建实例,我们知道抽象类不可以被实例化,但... 查看详情

java:java学习笔记之java单例模式的简单理解和使用(代码片段)

...单例的实现如下:2、懒汉式终极版本:volatile3、EffectiveJava1——静态内部类4、5.2EffectiveJava2——枚举参考Java单例模式1、饿汉式单例的实现如下://饿汉式实现publicclassSingleBprivatestaticfinalSingleBINSTA 查看详情

java:effectivejava学习笔记之考虑实现comparable接口(代码片段)

Java考虑实现Comparable接口考虑实现Comparable接口1、Comparable接口2、为什么要考虑实现Comparable接口3、compareTo方法的通用约定4、何时以及如何实现Comparable接口4.1、多重比较5、实现Comparable接口所需满足的需求6、总结参考考虑实现Compa... 查看详情

java:effectivejava学习笔记之覆盖equals时请遵守通用约定(代码片段)

Java覆盖equals时请遵守通用约定覆盖equals时请遵守通用约定1、为什么要覆盖equals2、需要覆盖equals方法的时机2.1、不需要覆盖equals方法的情况2.2、需要覆盖equals方法的情况2.2.1、自反性2.2.2、对称性2.2.3、传递性2.2.4、一致性2.2.5、... 查看详情

java:effectivejava学习笔记之接口只用于定义类型类层次优于标签类(代码片段)

Java接口只用于定义类型1、接口只用于定义类型1.1、常量接口2、类层次优于标签类1、接口只用于定义类型当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,就表明可以对这个类的实... 查看详情

java:effectivejava学习笔记之列表优先于数组(代码片段)

Java列表优先于数组列表优先于数组1、协变与不可变类型2、运行时检验与编译器检验3、可具体化与不可具体化4、无法很好混用列表和数组5、案例分析参考列表优先于数组1、协变与不可变类型1、数组是协变类型,指继承关... 查看详情

java:effectivejava学习笔记之请不要在新代码中使用原生态类型(代码片段)

Java请不要在新代码中使用原生态类型1、请不要在新代码中使用原生态类型参考1、请不要在新代码中使用原生态类型1、在没有泛型之前,从集合中读取到每一个对象都必须进行转换。如果有人不小心插入了类型错误的对象&#x... 查看详情

java:effectivejava学习笔记之消除过期对象引用(代码片段)

Java消除过期对象引用消除过期对象引用1、Java的垃圾回收机制2、Java中的内存泄露3、常见的内存泄露参考消除过期对象引用很多人可能在想这么一个问题:Java有垃圾回收机制,那么还存在内存泄露吗?答案是肯定的&#... 查看详情

java:effectivejava学习笔记之接口优于抽象类(代码片段)

Java接口优于抽象类接口优于抽象类1、接口和抽象类2、接口优点3、骨架类3.1、demo参考接口优于抽象类1、接口和抽象类Java中抽象类和接口的区别2、接口优点1、现有的类可以很容易的被更新,以实现新的接口。如果你前期编... 查看详情

java:effectivejava学习笔记之静态工厂方法的简单理解和使用(代码片段)

Java静态工厂方法静态工厂方法一、什么是静态工厂方法?二、静态工厂方法的优势1、静态工厂方法与构造器不同的第一优势在于,它们有名字2、静态工厂方法不用在每次调用的时候都创建一个新的对象3、静态工厂方法... 查看详情

effectivejava第十章并发避免过度同步读书笔记

 避免过度同步为了避免活性失败和安全性失败,再一个被同步的方法或者代码快中,永远不要放弃对客户端的控制。因为外来的,属于不可控的将外来方法调用移出同步的代码快。建立快照使用并发集合,CopyOnWriteArayList。... 查看详情

java:effectivejava学习笔记之覆盖equals时总要覆盖hashcode(代码片段)

Java覆盖equals时总要覆盖hashcode覆盖equals时总要覆盖hashcode1.什么是hashcode方法?2.hashcode相等与对象相等之间的关系:(保证设计是规范的前提下)3.为什么要覆盖hashcode3.1、覆盖equals时总要覆盖hashCode3.2、如何在覆盖... 查看详情

java:effectivejava学习笔记之通过私有构造器强化不可实例化的能力(代码片段)

Java通过私有构造器强化不可实例化的能力通过私有构造器强化不可实例化的能力参考通过私有构造器强化不可实例化的能力并非所有的类都是需要实例化的。有时候,我们可能需要编写至包含静态方法和静态域的类。这些类... 查看详情

java:effectivejava学习笔记之复合优先于继承(代码片段)

Java复合优先于继承复合优先于继承1、实现继承和接口继承2、在实际开发中继承的缺点2.1、子类依赖于其超类中特定功能的实现细节3、什么是复合?3.1、书上案例4、复合相比较于继承的优点和缺点5、何时使用继承,何时使用... 查看详情