工作5年的程序员感慨:finalfinallyfinalize面试这么卷?

Posted 跟着Mic学架构

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了工作5年的程序员感慨:finalfinallyfinalize面试这么卷?相关的知识,希望对你有一定的参考价值。

面试题: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

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

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

15: iinc          1, 1
18: iload_2
19: ireturn
20: astore_2

从上述指令的图解过程中可以看到,在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方法,是一个类被回收期间可能会被调用的方法。

问题总结

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

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

关注[跟着Mic学架构]公众号,获取更多精品原创

工作一年多的感慨与总结

前言 

博文断更了一月左右,期间是由于跳槽、离职、租房等等各种事耽误了,今天本来想写些技术的东西,但是突然觉得:从2017毕业到现在至始至终没有分享或记录过自己的一些心情杂事,都是些技术博文。

其实,早就想分享下自己这一年的收获,或多或少做些回忆,同时鞭策自己以后更加努力,好了,啰啰嗦嗦写了很多“流水账”,有点乱,希望能谅解!

 


 

2017/7-2017/9  找工作面试感慨

  不像大多数应届毕业生,参加校招之后进入比较理想的公司。在2017/7月25号因为一些变故放弃了一个比较稳定的工作,重新收拾心情回到家自学(基本都是刷面试笔试题)了15天,8月10号左右来到KM,当时身无分文借住 于一位好友租房中,找工作方面:笔试->面试->刷题->总结,心情则是这样的:期待->啥玩意!?工资3k!??->自我怀疑,啥我咋这都不会,我大学学了啥->啥!!看我不错,让我做一年实习生,转不转正到时候看我表现->啥?你知道我们公司最重要的是什么吗?是感恩,是狼性文化->没事的,先养活自己要紧同时学技术!

  由于走的是社招,虽然没有特意表明应届生的身份,但是仍然每次都会被贴上“应届生”的身份,总是被告之“我是来学习,不是来工作的”。前前后后10多家公司,里面有太多心酸,比如被HR催面试,结果花了很多钱打车到人家公司楼下面试,被突然告之老板出差下个月再说等,当然这并不是说这样的公司很多,可能是我运气有点背,当时也急于找工作稳定下来。

  最后,找到一个当地算是零售/饮食行业算是不错的公司,从事“企业信息化”相关的开发,最主要的是提供住宿!提供住宿!提供住宿! (所以我是不会提工资的)第二天早晨6点起床,一个人背上自己的行李,拖着重重的行李箱步行到公司大巴上车处,当时已是快下午1点,随后坐公司大巴来到荒无人烟的公司总部大楼前,收拾好心情入职报道!

2017/9-2018/9 一年职场感慨

  一切看似算时顺利吧,但入职这一年期间发生了很多让我震惊的事情:

  (1)开发团队=我自己

    之前面试的时候说到:除了我一个开发人员以外,还有个高级开发。算是我未见面的师傅,结果后来才发现是早就已经提了离职,等人来入职后才离职的,我算是来填坑的吗????(黑人问号.gif),果然后面一天人家就来办正式离职了,一堆文件交接给我就走了!

  (2)开发技术面试官虽是开发经理,但已10多年不编程。

    也就是我的顶头上司就是开发经理,他下手也就只有我一个初级开发工程师!面试的时候说是有啥开发的问题都可以找他,后来发现却只是管理项目,并不是参与编程,也不是很懂编程,停留在07/08年开发水平与知识,但是数据库方面确实很牛逼,值得我学习!

  (3)信息部门刚成立两年,据说公司两年前还在用手工录单(电商方面单子)

    这让我深刻体会到一个当代企业建立并完善信息化的重要性,据说我们部门成立后,引进了CRM、WMS等系统之后,至少提升了公司50%的营销量,然而正如部门老大所说:往往引入信息化在以销售为主的公司是一下子看不到成绩的,需要强硬的数据才能展示,所以emmm.......部门当然没有收到很好的重视,但大家似乎都知道这个道理仍然努力着!

  (4)第一次觉得跟人沟通这么难

    当时接受一个外包人员写的系统(系统各种烂就不说了,从按计划2018/2验收到2018/7bug才解决完,勉强验收),需要跟工厂里工人师傅与外包人员对接,单纯的我以为都是我们都是底层人员,肯定很好沟通的。结果写个文档错别字让我看不懂,每次都需要修改错别字,之后却被骂之:你以为你是老师吗?很牛逼嘛,改我写的错别字!当然,我还会常常在群里“劝架(这个师傅脾气很暴躁,经常撕外包开发的兄弟,两人经常在有领导的群里开撕)”。

    最好笑的是一会我是这个师傅的兄弟(常常替他解围,跟外包人员沟通),一会我是叛徒(有点可笑呢,说我替外包人员说话),一会就开始问候我妈了(当时我已经做好辞职的打算了,跑到工厂里必须要跟他打一架),但是被比我晚两个月份入职的高级开发,也就是我师傅拦住了,跟我说了好多。最后算是平静下来了(这后面当然还有更搞笑的,哈哈哈,之后觉得有趣的朋友可以评论,我有时间再写),哦对了,这时的高级开发算是在初入职场的贵人吧,之后会写到他给我一些感慨,还有我从他身上学到一些东西!

  当然,并不是奇葩震惊的事居多,这一年时间内我也会学会了很多,也要感谢很多当时离职没有当面说谢谢的人!

----------------------------------------------------------------------分割线(今天先暂时写到这吧,有时间再写下一篇)------------------------------------------------------------------------------------------------------------


以上是关于工作5年的程序员感慨:finalfinallyfinalize面试这么卷?的主要内容,如果未能解决你的问题,请参考以下文章

大厂的人自带光环,但光环是从哪儿来的呢?——一位中年Java程序员的感慨

大厂的人自带光环,但光环是从哪儿来的呢?——一位中年Java程序员的感慨

5年经验Java程序员面试20天感慨,巨头技术面试,为什么那么喜欢考算法

工作5年的Java程序员,才学会阅读源码,可悲吗?

工作一到五年的Java程序员遇到瓶颈应该如何提升

在北京工作5年的程序员,通过人才引进入职县城事业编,月薪曝光