美文网首页
工作5年的程序员感慨:final、finally、finaliz

工作5年的程序员感慨:final、finally、finaliz

作者: 跟着Mic学架构 | 来源:发表于2021-11-04 18:22 被阅读0次
    图怪兽_c3868f4629b4140e58dd0e1a27859523_50225

    面试题:final、finally、finalize的区别

    面试考察点

    考察目的: 了解求职者对Java基础的了解。

    考察范围: 工作1-3年的Java程序员。

    背景知识

    final/finally在工作中几乎无时无刻不再使用,因此即便是没有系统化的梳理这个问题,也能回答出一些内容。

    但是finalize就接触得非常少,接下来我们对这几个关键字逐一进行分析。

    final关键字

    final关键字代表着不可变性。

    面试题系列:工作5年,第一次这么清醒的理解final关键字?.这篇文章中, 我详细的进行了分析,建议大家去看这篇文章,这里就不重复分析了。

    finally关键字

    finally关键字用在try语句块后面,它的常用形式是

    try{
      
    }catch(){
      
    }finally{
      
    }
    

    以及下面这种形式。

    try{
      
    }finally{
      
    }
    

    finally语句块中的代码,无论try或者catch代码块中是否有异常,finally语句块中的代码一定会被执行,所以它一般用于清理工作、关闭链接等类型的语句。

    它的特点:

    1. finally语句一定会伴随try语句出现。
    2. try语句不能单独使用,必须配合catch语句或finally语句。
    3. try语句可以单独与catch语句一起使用,也可以单独与finally语句一起使用,也可以三者一起使用。

    finally 实战思考

    为了加深大家对于finally关键字的理解,我们来看下面这段代码。

    思考一下,下面这段代码,打印的结果分别是多少?

    public class FinallyExample {
        
        public static void main(String arg[]){
            System.out.println(getNumber(0));
            System.out.println(getNumber(1));
            System.out.println(getNumber(2));
            System.out.println(getNumber(4));
        }
        public static int getNumber(int num){
            try{
                int result=2/num;
                return result;
            }catch(Exception exception){
                return 0;
            }finally{
                if(num==0){
                    return -1;
                }
                if(num==1){
                    return 1;
                }
            }
        }
    }
    

    正确答案分别是:

    1. -1: 传入num=0,此时会报错java.lang.ArithmeticException: / by zero。因此进入到catch捕获该异常。由于finally语句块一定会被执行,因此进入到finally语句块,返回-1
    2. 1:传入num=1,此时程序运行正常,由于finally语句块一定会被执行,因此进入到finally代码块,得到结果1
    3. 1:传入num=2,此时程序运行正常,result=1,由于finally语句块一定会被执行,因此进入到finally代码块,但是finally语句块并没有触发对结果的修改,所以返回结果为1
    4. 0:传入num=4,此时程序运行正常,result=0(因为2/4=0.5,转换为int后得到0),由于finally语句块一定会被执行,因此进入到finally代码块,但是finally语句块并没有触发对结果的修改,所以返回结果为0

    什么情况下finally不会执行

    finally代码块,是否有存在不会被执行的情况呢?

    System.exit()

    来看下面这段代码:

    public class FinallyExample {
    
        public static void main(String arg[]){
            System.out.println(getNumber(0));
        }
        public static int getNumber(int num){
            try{
                int result=2/num;
                return result;
            }catch(Exception exception){
                System.out.println("触发异常执行");
                System.exit(0);
                return 0;
            }finally{
                System.out.println("执行finally语句块");
            }
        }
    }
    

    catch语句块中,增加了System.exit(0)代码,执行结果如下

    触发异常执行
    

    可以发现,在这种情况下,并没有执行finally语句块。

    扩展知识,为什么System.exit(0)会破坏finally呢?

    来看一下源码以及注释。

    /**
         * Terminates the currently running Java Virtual Machine. The
         * argument serves as a status code; by convention, a nonzero status
         * code indicates abnormal termination.
         * <p>
         * This method calls the <code>exit</code> method in class
         * <code>Runtime</code>. This method never returns normally.
         * <p>
         * The call <code>System.exit(n)</code> is effectively equivalent to
         * the call:
         * <blockquote><pre>
         * Runtime.getRuntime().exit(n)
         * </pre></blockquote>
         *
         * @param      status   exit status.
         * @throws  SecurityException
         *        if a security manager exists and its <code>checkExit</code>
         *        method doesn't allow exit with the specified status.
         * @see        java.lang.Runtime#exit(int)
         */
    public static void exit(int status) {
      Runtime.getRuntime().exit(status);
    }
    

    该方法用来结束当前正在运行的Java JVM。如果 status 是非零参数,那么表示是非正常退出。

    1. System.exit(0) : 将整个虚拟机里的内容都关掉,内存都释放掉!正常退出程序。

    2. System.exit(1) : 非正常退出程序

    3. System.exit(-1) :非正常退出程序

    由于当前JVM已经结束了,因此程序代码自然不能继续执行。

    守护线程被中断

    先来看下面这段代码:

    public class FinallyExample {
    
        public static void main(String[] args) {
            Thread t = new Thread(new Task());
            t.setDaemon(true); //置为守护线程
            t.start();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException("the "+Thread.currentThread().getName()+" has been interrupted",e);
            }
        }
    }
    class Task implements Runnable {
        @Override
        public void run() {
            System.out.println("执行 run()方法");
            try {
                System.out.println("执行 try 语句块");
                TimeUnit.SECONDS.sleep(5); //阻塞5s
            } catch(InterruptedException e) {
                System.out.println("执行 catch 语句块");
                throw new RuntimeException("the "+Thread.currentThread().getName()+" has been interrupted",e);
            } finally {
                System.out.println("执行 finally 语句块");
            }
        }
    }
    

    运行结果如下:

    执行 run()方法
    执行 try 语句块
    

    从结果发现,finally语句块中的代码并没有被执行?为什么呢?

    守护线程的特性是:只要JVM中没有任何非守护线程在运行,那么虚拟机会kill掉所有守护线程从而终止程序。换句话说,就是守护线程是否正在运行,都不影响JVM的终止。

    在虚拟机中,垃圾回收线程以及main线程都是守护线程。

    在上述运行的程序中,执行逻辑描述如下:

    1. 线程t是守护线程,它开启一个任务Task执行,该线程tmain方法中执行,并且在睡眠1s之后,main方法执行结束
    2. Task是一个守护线程的执行任务,该任务睡眠5s。

    基于守护线程的特性,maintask都是守护线程,因此当main线程执行结束后,并不会因为Task这个线程还未执行结束而阻塞。而是在等待1s后,结束该进程。

    这就使得Task这个线程的代码还未执行完成,但是JVM进程已结束,所以finally语句块没有被执行。

    finally执行顺序

    基于上述内容的理解,是不是自认为对finally关键字掌握很好了?那我们在来看看下面这个问题。

    这段代码的执行结果是多少呢?

    public class FinallyExample2 {
    
      public int add() {
        int x = 1;
        try {
          return ++x;
        } catch (Exception e) {
          System.out.println("执行catch语句块");
          ++x;
        } finally {
          System.out.println("执行finally语句块");
          ++x;
        }
        return x;
      }
      public static void main(String[] args) {
        FinallyExample2 t = new FinallyExample2();
        int y = t.add();
        System.out.println(y);
      }
    }
    

    上述程序运行的结果是:2

    这个结果应该有点意外,因为按照finally的语义,首先执行try代码块,++x后得到的结果应该是2, 接着再执行finally语句块,应该是在2的基础上再+1,得到结果是3,那为什么是2?

    在解答这个问题之前,先来看一下这段代码的字节码,使用javap -v FinallyExample2.

     public int add();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=4, args_size=1
             0: iconst_1     //iconst 指令将常量压入栈中。
             1: istore_1     //
             2: iinc          1, 1  //执行++x操作
             5: iload_1       
             6: istore_2
             7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
            10: ldc           #3                  // String 执行finally语句块
            12: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            15: iinc          1, 1
            18: iload_2
            19: ireturn
            20: astore_2
            21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
            24: ldc           #6                  // String 执行catch语句块
            26: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            29: iinc          1, 1
            32: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
            35: ldc           #3                  // String 执行finally语句块
            37: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            40: iinc          1, 1
            43: goto          60
            46: astore_3
            47: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
            50: ldc           #3                  // String 执行finally语句块
            52: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            55: iinc          1, 1
            58: aload_3
            59: athrow
            60: iload_1
            61: ireturn
          Exception table:
             from    to  target type
                 2     7    20   Class java/lang/Exception
                 2     7    46   any
                20    32    46   any
    

    简单说明一下和本次案例分析有关的字节指令

    • iconst,把常量压入到栈中。
    • istore,栈顶的int数值存入局部变量表。
    • iload,把int类型的变量压入到栈顶。
    • iinc,对局部变量表中index为i的元素加上n。
    • ireturn,返回一个int类型的值。
    • astore,将一个数值从操作数栈存储到局部变量表。
    • athrow,抛出一个异常。
    • aload,将一个局部变量加载到操作栈。

    了解了这些指令之后,再来分析上述字节码的内容。

    先来看第一步分,这部分是try代码块中的指令。

    0: iconst_1     //iconst 指令将常量压入栈中。
    1: istore_1     //
    2: iinc          1, 1  //执行++x操作
    5: iload_1       
    6: istore_2
    

    上述指令的执行流程,图解如下。

    image-20211104164804716

    接下来继续往下看字节码,这个是在finally里面执行的指令。

    15: iinc          1, 1
    18: iload_2
    19: ireturn
    20: astore_2
    
    image-20211104173815447

    从上述指令的图解过程中可以看到,在finally语句块中虽然对x的值做了累加,但是最终返回的时候,仍然是2.

    后续剩余的指令,是异常表对应的执行指令,异常表的解读方式是:

    • 从2行到第7行,如果触发了Exception,则会跳转到20行的指令开始执行。

    • 从2行到第7行,如果触发了任何异常,则会跳转到46行开始执行。

    • 从20行到第32行,如果触发了任何异常,则会跳转到46行开始执行。

    Exception table:
      from    to  target type
        2     7    20   Class java/lang/Exception
        2     7    46   any
        20    32    46   any
    

    结论:从上述字节指令的执行过程中可以发现,当try中带有return时,会先执行return前的代码,然后暂时保存需要return的信息,再执行finally中的代码,最后再通过return返回之前保存的信息。所以这里运行的结果是2,而不是3。

    除此之外,还有其他的变体,比如:

    public class FinallyExample2 {
    
        public int add() {
            int x = 1;
            try {
                return ++x;
            } catch (Exception e) {
                System.out.println("执行catch语句块");
                ++x;
            } finally {
                System.out.println("执行finally语句块");
                ++x;
                return x;
            }
        }
        public static void main(String[] args) {
            FinallyExample2 t = new FinallyExample2();
            int y = t.add();
            System.out.println(y);
        }
    }
    

    那,这段代码运行结果是多少呢?

    打印结果如下:

    执行finally语句块
    3
    

    结论:当finally中有return的时候,try中的return会失效,在执行完finally的return之后,就不会再执行try中的return。不过不推荐在finally中写return,这会破坏程序的完整性,而且一旦finally里出现异常,会导致catch中的异常被覆盖。

    关于这个部分说解释的内容,在JVM的中有Exceptions and finally解释。

    If the try clause executes a return, the compiled code does the following:

    1. Saves the return value (if any) in a local variable.
    2. Executes a jsr to the code for the finally clause.
    3. Upon return from the finally clause, returns the value saved in the local variable.

    简单翻译如下:

    如果 try 语句里有 return,那么代码的行为如下:

    1. 如果有返回值,就把返回值保存到局部变量中
    2. 执行 jsr 指令跳到 finally 语句里执行
    3. 执行完 finally 语句后,返回之前保存在局部变量表里的值

    finalize方法

    finalize 方法定义在 Object 类中,其方法定义如下:

    protected void finalize() throws Throwable {
    }
    

    当一个类在被回收期间,这个方法就可能会被调用到。

    它有使用规则是:

    1. 当对象不再被任何对象引用时,GC会调用该对象的finalize()方法
    2. finalize()是Object的方法,子类可以覆盖这个方法来做一些系统资源的释放或者数据的清理
    3. 可以在finalize()让这个对象再次被引用,避免被GC回收;但是最常用的目的还是做cleanup
    4. Java不保证这个finalize()一定被执行;但是保证调用finalize的线程没有持有任何user-visible同步锁。
    5. 在finalize里面抛出的异常会被忽略,同时方法终止。
    6. 当finalize被调用之后,JVM会再一次检测这个对象是否能被存活的线程访问得到,如果不是,则清除该对象。也就是finalize只能被调用一次;也就是说,覆盖了finalize方法的对象需要经过两个GC周期才能被清除。

    问题回答

    面试题:final、finally、finalize的区别

    回答:

    1. final用来修饰类、方法、属性,被final修饰的类,表示该类无法被继承,被final修饰的属性,表示该属性无法被修改,被final修饰的方法,表示该方法无法被重写

    2. finally,它和try语句块组成一个完整的语法,表示一定会被执行的代码块,当然也有方式可以破坏它的执行特性

      1. 通过System.exit
      2. 守护线程的终止
    3. finalize方法,是一个类被回收期间可能会被调用的方法。

    问题总结

    一道面试题,要深挖下来,可以产生很多变体。

    这篇文章不一定非常全面的涵盖了所有可能的情况,但是各位读者一定要注意,只有体系化的知识,才能创造价值

    相关文章

      网友评论

          本文标题:工作5年的程序员感慨:final、finally、finaliz

          本文链接:https://www.haomeiwen.com/subject/demkzltx.html