本文转载自微信公众号“Java中文社区”,作者雷哥。转载本文请联系Java中文社区公众号。在Java语言中,try-catch-finally看似简单,看起来对人畜无害,但要真正“驾驭”它却不是一件容易的事。除此之外,让我们以fianlly为例。别看它功能单一,但使用起来却“暗藏杀机”。不信我们看下面的例子...坑一:finally如果finally中使用了return,那么即使在try-catch中有return操作,也不会立即返回结果,而是finally中的语句将在返回之前执行。至此,问题就来了:如果finally中有return语句,那么finally中的结果会直接返回,从而无情的丢弃掉try中的返回值。①反例代码publicstaticvoidmain(String[]args)throwsFileNotFoundException{System.out.println("执行结果:"+test());}privatestaticinttest(){intnum=0;try{//num=1,不返回这里num++;returnnum;}catch(Exceptione){//dosomething}finally{//num=2,返回这个值num++;returnnum;}}上面代码的执行结果如下:②原因分析如果有finally中的return语句,then中的try-catch中的返回值会被覆盖。如果程序员在编写代码时没有发现这个问题,就会导致程序的执行结果出错。③解决方法如果try-catch-finally中有return返回值,确保ret??urn语句只在方法结束时出现一次。④正码publicstaticvoidmain(String[]args)throwsFileNotFoundException{System.out.println("执行结果:"+testAmend());}privatestaticinttestAmend(){intnum=0;try{num=1;}catch(Exceptione){//dosomething}finally{//dosomething}//保证return语句在这里只出现一次returnnum;}坑二:finally中的代码“没有执行”如果上面的例子比较简单,那么下面的例子就给如果你有不同的感受,就看代码吧。①反例代码publicstaticvoidmain(String[]args)throwsFileNotFoundException{System.out.println("执行结果:"+getValue());}privatestaticintgetValue(){intnum=1;try{returnnum;}finally{num++;}}上面代码的执行结果如下:②原因分析原以为执行结果会是2,没想到是1。用马老师的话说:“我粗心,没闪”.可能有人会问:代码换成++num,结果会不会是2?不好意思告诉你,不对,执行的结果还是1。那这是为什么呢?要真正理解它,我们还得从这段代码的字节码说起。上述代码最终生成的字节码如下://classversion52.0(52)//accessflags0x21publicclasscom/example/basic/FinallyExample{//compiledfrom:FinallyExample.java//accessflags0x1public()VL0LINENUMBER5L0ALOAD0INVOKESPECIALjava/lang/Object。/StringBuilder.()VLDC"\u6267\u884c\u7ed3\u679c:"INVOKEVIRTUALjava/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;INVOKESTATICcom/example/basic/FinallyExample.getValue()IINVOKEVIRTUALjava/lang/StringBuilder.append(I)Ljava/lang/StringBuilder;INVOKEVIRTUALjava/lang/StringBuilder.toString()Ljava/lang/String;INVOKEVIRTUALjava/io/PrintStream.println(Ljava/lang/String;)VL1LINENUMBER14L1RETURNL2LOCALVARIABLEargs[Ljava/lang/String;L0L20MAXSTACK=3MAXLOCALS=1//accessflags0xAprivatestaticgetValue()ITRYCATCHBLOCKL0L1L2nullL3LINENUMBER18L3ICONST_1ISTORE0L0LINENUMBER20L0ILOAD0ISTORE1L1LINENUMBER22L1IINC01L4LINENUMBER20L4ILOAD1IRETURNL2LINENUMBER22L2FRAMEFULL[I][java/lang/Throwable]ASTORE2IINC01L5LINENUMBER23L5ALOAD2ATHROWL6LOCALVARIABLEnumIL0L60MAXSTACK=1MAXLOCALS=3}这些字节码的简易版本如下图所示:想要读要理解这些字节码,首先要理解这些字节码所代表的含义。这些内容可以从Oracle官网查询到(英文文档):https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html这里雷哥对这些字节码做了一个简单的翻译:iconst就是将int类型的值压入操作数栈;istore是将int存储到一个局部变量中。iload从局部变量加载一个int值。iinc通过下标递增局部变量。ireturn从操作数栈中返回一个int类型的值。astore将引用存储到局部变量中。有了这些信息,我们再翻译一下上面的字节码内容:0iconst_1将值存入操作数栈11istore_0将操作数栈中的数据存入局部变量02iload_0从局部变量中读取值到操作数栈3istore_1将1存入局部变量14iinc0by1位置的操作数栈自增(+1)局部变量0位置的元素7iload_1加载局部位置1的值到操作数栈8ireturn返回操作数栈中的int通过以上信息,您可能无法直观地看到该方法的内部执行过程。没关系,雷哥给你准备了一张方法执行流程图:从上图我们可以看到,在执行finally语句(iinc0,1)之前,局部变量表中存放了两条信息,位置0和位置1都存储了一个值为1的int值,在执行finally(iinc0,1)之前,只累加位置0的值,然后将位置1的值(1)返回给操作数栈,所以执行返回操作(ireturn)时,从栈中读取操作返回值1,所以最后执行的是1而不是2。③解决方案关于Java虚拟机如何编译finally语句块,有兴趣的读者可以参考《The JavaTM Virtual Machine Specification, Second Edition》中的7.13编译finally。Java虚拟机如何编译finally语句块里面有详细的描述。实际上,Java虚拟机是在控制权转移之前直接插入finally语句块作为子程序(我不知道这个子程序怎么翻译,干脆不翻译了,以免产生歧义和误解)try语句块或catch语句块的语句。但是,还有一个因素不容忽视,就是在执行子程序之前(即finally语句块),try或catch语句块会在局部变量表(LocalVariableTable)中保留其返回值,直到子程序执行完后,将保留的返回值恢复到操作数栈,再通过return或throw语句返回给方法的调用者(invoker)。因此,如果try-catch-finally中有return操作,**必须保证return语句只在方法结束时出现一次!**这样可以保证try-catch-finally中的所有操作代码都会执行影响。④正例代码privatestaticintgetValueByAmend(){intnum=1;try{//dosomething}catch(Exceptione){//dosomething}finally{num++;}returnnum;}坑三:finally中的代码是“notfinal”执行的①负例示例代码publicstaticvoidmain(String[]args)throwsFileNotFoundException{execErr();}privatestaticvoidexecErr(){}catch(RuntimeExceptione){e.printStackTrace();}finally{System.out.println("最后执行。");}}上面代码的执行结果如下:从上面的结果可以看出,finally中的代码并不是最后执行的,而是在catch打印异常之前执行的,为什么呢?②原因分析出现上述问题的真正原因其实并不是try-catch-finally。当我们打开e.printStackTrace的源码时,可以看到一些端倪。源码如下:从上图可以看出,在执行.printStackTrace()和最后输出信息的时候,并没有使用同一个对象。finally使用标准输出流:System.out,但是e.printStackTrace()使用标准错误输出流:System.err.println,它们的执行等价于:publicstaticvoidmain(String[]args){System.out.println("我是标准输出流");System.err.println("Iamthestandarderroroutputstream");}以上代码执行结果的顺序也是随机的,而这一切的原因,我们可以从注释和文档中看出标准错误输出流(System.err):我们简单的对上面的注释做一个简单的翻译:“标准”错误输出流。流已打开并准备好接受输出数据。通常,此流对应于主机环境或用户指定的显示输出或另一个输出目标。按照惯例,即使主输出流(out输出流)已被重定向到文件或其他目标,此输出流(err输出流)也可用于显示错误消息或其他应该吸引用户的信息立即关注。从源码的注释信息可以看出,标准错误输出流(System.err)和标准输出流(System.out)使用不同的流对象,即使标准输出流定位到其他文件,它不会影响标准错误输出流。那么我们可以大胆猜测一下:两者是独立执行的,为了更高效的输出流信息,两者是并行执行的,所以我们看到的结果是打印顺序总是随机的。为了验证这个观点,我们将标准输出流重定向到一个文件中,然后观察是否可以正常打印System.err。实现代码如下:publicstaticvoidmain(String[]args)throwsFileNotFoundException{//在System.setOut(newPrintStream(newFileOutputStream("log.txt")))中找到标准输出流的信息到log.txt中System.out.println("我是标准输出流");System.err.println("Iamthestandarderroroutputstream");}上面代码的执行结果如下:程序执行后,我们发现根目录下出现了一个新的log.txt文件的项目。打开这个文件,可以看到如下结果:从上面的结果我们可以看出,标准输出流和标准错误输出流是相互独立执行的,JVM为了高效执行会将它们并行运行,所以我们看到的最终结果是finally在catch之前执行。③解决办法如果知道了原因,那么问题就很容易处理了。我们只需要将try-catch-finally中的输出对象改为统一的输出流对象就可以解决这个问题。④正例代码privatestaticvoidexecErr(){try{thrownewRuntimeException();}catch(RuntimeExceptione){System.out.println(e);}finally{System.out.println("Executefinally.");}}是改为统一输出流对象后,我手动执行了n次,没发现问题。坑4:finally中的代码“不执行”finally中的代码会被执行吗?如果是以前,我会毫不犹豫的说“是”,但被社会狠狠打了一顿之后,我可能会这样回答:一般情况下,finally中的代码会被执行,但特殊情况下,finally中的代码不一定会执行,比如以下几种情况:System.exit是在try-catch语句中执行的;在try-catch语句中存在死循环;在最终执行之前断电或JVM崩溃。如果出现以上任何一种情况,finally中的代码将不会被执行。虽然我觉得这篇文章有点“抬杠”的嫌疑,但墨菲定律告诉我们,如果某件事很可能发生,那么它就一定会发生。所以从严谨的角度来看,这个观点还是成立的,尤其是对于新手来说,很容易写出不知不觉就找不到的无限循环,不是吗?①反例代码publicstaticvoidmain(String[]args){noFinally();}privatestaticvoidnoFinally(){try{System.out.println("Iamtry~");System.exit(0);}catch(Exceptione){//dosomething}finally{System.out.println("Iamfinally~");}}上面代码的执行结果如下:从上面的结果可以看出finally中的代码并没有执行.②解决方案排除代码中的System.exit代码,除非是业务需要,但也要注意,如果try-cacth中出现System.exit代码,finally中的代码将不会被执行。总结一下这篇文章,我们在finally中展示了一些问题,有很实用的干货,也有一些看似“优秀”的例子,但是这些都从侧面证明了一件事,那就是try-catch-finally这不是一件容易的事。最后要强调的是,如果try-catch-finally中有return返回值操作,那么一定要保证return语句只在方法的最后出现一次!参考&感谢阿里巴巴《Java开发手册》developer.ibm.com/zh/articles/j-lo-finally