怎么样,发现了没?就问你神不神奇?
在源码中,只在 finally 代码块出现过一次的输出语句,在字节码中出现了三次。
finally 代码块中的代码被复制了两份,分别放到了 try 和 catch 语句的后面。再配合异常表使用,就能达到 finally 语句一定会被执行的效果。
以后再也不怕面试官问你为什么 finally 一定会执行了。
虽然应该也没有面试官会问这样无聊的问题。
问起来了,就从字节码的角度给他分析一波。
当然了,如果你非要给我抬个杠,聊聊 System.exit
的情况,就没多大意义了。
最后,关于 finally,再讨论一下这个场景:
public class MainTest { public static void main(String[] args) { try { int a = 1 / 0; } finally { System.out.println("final"); } } }
这个场景下,没啥说的, try 里面抛出异常,触发 finally 的输出语句,然后接着被抛出去,打印在控制台:
如果我在 finally 里面加一个 return 呢?
可以看到,运行结果里面异常都没有被抛出来:
为什么呢?
答案就藏在字节码里面:
其实已经一目了然了。
右边的 finally 里面有 return,并没有 athrow 指令,所以异常根本就没有抛出去。
这也是为什么建议大家不要在 finally 语句里面写 return 的原因之一。
冷知识
再给大家补充一个关于异常的冷知识吧。
还是上面这个截图。你有没有觉得有一丝丝的奇怪?
夜深人静的时候,你有没有想过这样的一个问题:
程序里面并没有打印日志的地方,那么控制台的日子是谁通过什么地方打印出来的呢?
是谁干的?
这个问题很好回答,猜也能猜到,是 JVM 帮我们干的。
什么地方?
这个问题的答案,藏在源码的这个地方,我给你打个断点跑一下,当然我建议你也打个断点跑一下:
java.lang.ThreadGroup#uncaughtException
而在这个地方打上断点,根据调用堆栈顺藤摸瓜可以找到这个地方:
java.lang.Thread#dispatchUncaughtException
看方法上的注释:
This method is intended to be called only by the JVM.
翻译过来就是:这个方法只能由 JVM 来调用。
既然源码里面都这样说了,我们可以去找找对应的源码嘛。
https://hg.openjdk.java.net/jdk7u/jdk7u/hotspot/file/5b9a416a5632/src/share/vm/runtime/thread.cpp
在 openJdk 的 thread.cpp 源码里面确实是找到了该方法被调用的地方:
而且这个方法还有个有意思的用法。
看下面的程序和输出结果:
我们可以自定义当前线程的 UncaughtExceptionHandler
,在里面做一些兜底的操作。
有没有品出来一丝丝全局异常处理机制的味道?
好了,再来最后一个问题:
我都这样问了,那么答案肯定是不一定的。
你就想想,发挥你的小脑袋使劲的想,啥情况下 try 里面的代码抛出了异常,外面的 catch 不会捕捉到?
来,看图:
没想到吧?
这样处理一下,外面的 catch 就捕捉不到异常了。
是不是很想打我。
别慌,上面这样套娃多没意思啊。
你再看看我这份代码:
public class MainTest { public static void main(String[] args) { try { ExecutorService threadPool = Executors.newFixedThreadPool(1); threadPool.submit(()->{ int a=1/0; }); } catch (Exception e) { e.printStackTrace(); } } }
你直接拿去执行,控制台不会有任何的输出。
来看动图:
是不是很神奇?
不要慌,还有更绝的。
把上面的代码从 threadPool.submit
修改为 threadPool.execute
就会有异常信息打印出来了:
但是你仔细看,你会发现,异常信息虽然打印出来了,但是也不是因为有 catch 代码块的存在。
具体是为啥呢?
参见这篇文章,我之前详细讲过的:《关于多线程中抛异常的这个面试题我再说最后一次!》
最后说一句
好了,看到了这里安排个点赞吧。感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。