面试题:final、finally、finalize的区别
考察目的: 了解求职者对Java基础的了解。
考察范围: 工作1-3年的Java程序员。
final
/finally
在工作中几乎无时无刻不再使用,因此即便是没有系统化的梳理这个问题,也能回答出一些内容。
但是finalize
就接触得非常少,接下来我们对这几个关键字逐一进行分析。
final关键字代表着不可变性。
在面试题系列:工作5年,第一次这么清醒的理解final关键字?.这篇文章中, 我详细的进行了分析,建议大家去看这篇文章,这里就不重复分析了。
finally
关键字用在try
语句块后面,它的常用形式是
try{ }catch(){ }finally{ }
以及下面这种形式。
try{ }finally{ }
finally语句块中的代码,无论try或者catch代码块中是否有异常,finally语句块中的代码一定会被执行,所以它一般用于清理工作、关闭链接等类型的语句。
它的特点:
finally
语句一定会伴随try
语句出现。try
语句不能单独使用,必须配合catch
语句或finally
语句。try
语句可以单独与catch
语句一起使用,也可以单独与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
: 传入num=0
,此时会报错java.lang.ArithmeticException: / by zero
。因此进入到catch
捕获该异常。由于finally
语句块一定会被执行,因此进入到finally
语句块,返回-1
。1
:传入num=1
,此时程序运行正常,由于finally
语句块一定会被执行,因此进入到finally
代码块,得到结果1
。1
:传入num=2
,此时程序运行正常,result=1
,由于finally
语句块一定会被执行,因此进入到finally
代码块,但是finally
语句块并没有触发对结果的修改,所以返回结果为1
。0
:传入num=4
,此时程序运行正常,result=0
(因为2/4=0.5,转换为int后得到0),由于finally
语句块一定会被执行,因此进入到finally
代码块,但是finally
语句块并没有触发对结果的修改,所以返回结果为0
。finally
不会执行finally
代码块,是否有存在不会被执行的情况呢?
来看下面这段代码:
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 是非零参数,那么表示是非正常退出。
System.exit(0) : 将整个虚拟机里的内容都关掉,内存都释放掉!正常退出程序。
System.exit(1) : 非正常退出程序
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线程都是守护线程。
在上述运行的程序中,执行逻辑描述如下:
t
是守护线程,它开启一个任务Task
执行,该线程t
在main
方法中执行,并且在睡眠1s之后,main
方法执行结束Task
是一个守护线程的执行任务,该任务睡眠5s。基于守护线程的特性,main
和task
都是守护线程,因此当main
线程执行结束后,并不会因为Task
这个线程还未执行结束而阻塞。而是在等待1s后,结束该进程。
这就使得Task
这个线程的代码还未执行完成,但是JVM进程已结束,所以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:
- Saves the return value (if any) in a local variable.
- Executes a jsr to the code for the finally clause.
- Upon return from the finally clause, returns the value saved in the local variable.
简单翻译如下:
如果 try 语句里有 return,那么代码的行为如下:
finalize 方法定义在 Object 类中,其方法定义如下:
protected void finalize() throws Throwable { }
当一个类在被回收期间,这个方法就可能会被调用到。
它有使用规则是:
面试题:final、finally、finalize的区别
回答:
final用来修饰类、方法、属性,被final修饰的类,表示该类无法被继承,被final修饰的属性,表示该属性无法被修改,被final修饰的方法,表示该方法无法被重写
finally,它和try语句块组成一个完整的语法,表示一定会被执行的代码块,当然也有方式可以破坏它的执行特性
finalize方法,是一个类被回收期间可能会被调用的方法。
一道面试题,要深挖下来,可以产生很多变体。
这篇文章不一定非常全面的涵盖了所有可能的情况,但是各位读者一定要注意,只有体系化的知识,才能创造价值
关注[跟着Mic学架构]公众号,获取更多精品原创