jdk7以后的异常处理try-with-resource关闭资源-try-catch-finally使用中的问题总结-《Java笔记》

admin 2025-10-19 01:47:42 编程 来源:ZONE.CI 全球网 0 阅读模式
  • 坑1:finally中使用return
  • 坑2:finally中的代码“不执行”
    • ①%20反例代码
    • ②%20原因分析
    • ③%20解决方案
    • ④%20正例代码
  • 坑3:finally中的代码“非最后”执行
    • ①%20反例代码
    • ②%20原因分析
    • ③%20解决方案
    • ④%20正例代码
  • 坑4:finally中的代码“不执行”
    • ①%20反例代码
    • ②%20解决方案

    Java%20try-catch-finally

    坑1:finally中使用return

    若在%20finally%20中使用%20return,那么即使%20try-catch%20中有%20return%20操作,也不会立马返回结果,而是再执行完%20finally%20中的语句再返回。此时问题就产生了:如果%20finally%20中存在%20return%20语句,则会直接返回%20finally%20中的结果,从而无情的丢弃了%20try%20中的返回值。

    ①%20反例代码public%20static%20void%20main(String[]%20args)%20throws%20FileNotFoundException%20{%20%20%20%20System.out.println("执行结果:"%20+%20test());}private%20static%20int%20test()%20{%20%20%20%20int%20num%20=%200;%20%20%20%20try%20{%20%20%20%20%20%20%20%20//%20num=1,此处不返回%20%20%20%20%20%20%20%20num++;%20%20%20%20%20%20%20%20return%20num;%20%20%20%20}%20catch%20(Exception%20e)%20{%20%20%20%20%20%20%20%20//%20do%20something%20%20%20%20}%20finally%20{%20%20%20%20%20%20%20%20//%20num=2,返回此值%20%20%20%20%20%20%20%20num++;%20%20%20%20%20%20%20%20return%20num;%20%20%20%20}}

    以上代码的执行结果如下:

    ②%20原因分析

    如果在%20finally%20中存在%20return%20语句,那么%20try-catch%20中的%20return%20值都会被覆盖,如果程序员在写代码的时候没有发现这个问题,那么就会导致程序的执行结果出错。

    ③%20解决方案

    如果%20try-catch-finally%20中存在%20return%20返回值的情况,一定要确保%20return%20语句只在方法的尾部出现一次

    ④%20正例代码public%20static%20void%20main(String[]%20args)%20throws%20FileNotFoundException%20{%20%20%20%20System.out.println("执行结果:"%20+%20testAmend());}private%20static%20int%20testAmend()%20{%20%20%20%20int%20num%20=%200;%20%20%20%20try%20{%20%20%20%20%20%20%20%20num%20=%201;%20%20%20%20}%20catch%20(Exception%20e)%20{%20%20%20%20%20%20%20%20//%20do%20something%20%20%20%20}%20finally%20{%20%20%20%20%20%20%20%20//%20do%20something%20%20%20%20}%20%20%20%20//%20确保%20return%20语句只在此处出现一次%20%20%20%20return%20num;}

    坑2:finally中的代码“不执行”

    如果说上面的示例比较简单,那么下面这个示例会有不同的感受,直接来看代码。

    ①%20反例代码public%20static%20void%20main(String[]%20args)%20throws%20FileNotFoundException%20{%20%20%20%20System.out.println("执行结果:"%20+%20getValue());}private%20static%20int%20getValue()%20{%20%20%20%20int%20num%20=%201;%20%20%20%20try%20{%20%20%20%20%20%20%20%20return%20num;%20%20%20%20}%20finally%20{%20%20%20%20%20%20%20%20num++;%20%20%20%20}}

    以上代码的执行结果如下:

    ②%20原因分析

    本以为执行的结果会是%202,但万万没想到竟然是%201。有人可能会问:如果把代码换成%20++num,那么结果会不会是%202%20呢?答案是并不会,执行的结果依然是%201。那为什么会这样呢?想要真正的搞懂它,就得从这段代码的字节码说起了。以上代码最终生成的字节码如下:

    //%20class%20version%2052.0%20(52)//%20access%20flags%200x21public%20class%20com/example/basic/FinallyExample%20{%20%20//%20compiled%20from:%20FinallyExample.java%20%20//%20access%20flags%200x1%20%20public%20<init>()V%20%20%20L0%20%20%20%20LINENUMBER%205%20L0%20%20%20%20ALOAD%200%20%20%20%20INVOKESPECIAL%20java/lang/Object.<init>%20()V%20%20%20%20RETURN%20%20%20L1%20%20%20%20LOCALVARIABLE%20this%20Lcom/example/basic/FinallyExample;%20L0%20L1%200%20%20%20%20MAXSTACK%20=%201%20%20%20%20MAXLOCALS%20=%201%20%20//%20access%20flags%200x9%20%20public%20static%20main([Ljava/lang/String;)V%20throws%20java/io/FileNotFoundException%20%20%20%20L0%20%20%20%20LINENUMBER%2013%20L0%20%20%20%20GETSTATIC%20java/lang/System.out%20:%20Ljava/io/PrintStream;%20%20%20%20NEW%20java/lang/StringBuilder%20%20%20%20DUP%20%20%20%20INVOKESPECIAL%20java/lang/StringBuilder.<init>%20()V%20%20%20%20LDC%20"\u6267\u884c\u7ed3\u679c:"%20%20%20%20INVOKEVIRTUAL%20java/lang/StringBuilder.append%20(Ljava/lang/String;)Ljava/lang/StringBuilder;%20%20%20%20INVOKESTATIC%20com/example/basic/FinallyExample.getValue%20()I%20%20%20%20INVOKEVIRTUAL%20java/lang/StringBuilder.append%20(I)Ljava/lang/StringBuilder;%20%20%20%20INVOKEVIRTUAL%20java/lang/StringBuilder.toString%20()Ljava/lang/String;%20%20%20%20INVOKEVIRTUAL%20java/io/PrintStream.println%20(Ljava/lang/String;)V%20%20%20L1%20%20%20%20LINENUMBER%2014%20L1%20%20%20%20RETURN%20%20%20L2%20%20%20%20LOCALVARIABLE%20args%20[Ljava/lang/String;%20L0%20L2%200%20%20%20%20MAXSTACK%20=%203%20%20%20%20MAXLOCALS%20=%201%20%20//%20access%20flags%200xA%20%20private%20static%20getValue()I%20%20%20%20TRYCATCHBLOCK%20L0%20L1%20L2%20null%20%20%20L3%20%20%20%20LINENUMBER%2018%20L3%20%20%20%20ICONST_1%20%20%20%20ISTORE%200%20%20%20L0%20%20%20%20LINENUMBER%2020%20L0%20%20%20%20ILOAD%200%20%20%20%20ISTORE%201%20%20%20L1%20%20%20%20LINENUMBER%2022%20L1%20%20%20%20IINC%200%201%20%20%20L4%20%20%20%20LINENUMBER%2020%20L4%20%20%20%20ILOAD%201%20%20%20%20IRETURN%20%20%20L2%20%20%20%20LINENUMBER%2022%20L2%20%20%20FRAME%20FULL%20[I]%20[java/lang/Throwable]%20%20%20%20ASTORE%202%20%20%20%20IINC%200%201%20%20%20L5%20%20%20%20LINENUMBER%2023%20L5%20%20%20%20ALOAD%202%20%20%20%20ATHROW%20%20%20L6%20%20%20%20LOCALVARIABLE%20num%20I%20L0%20L6%200%20%20%20%20MAXSTACK%20=%201%20%20%20%20MAXLOCALS%20=%203}

    这些字节码的简易版本如下图所示:想要读懂这些字节码,首先要搞懂这些字节码所代表的含义,这些内容可以从%20Oracle%20的官网查询到(英文文档):https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html磊哥在这里对这些字节码做一个简单的翻译:

    iconst%20是将%20int%20类型的值压入操作数栈。istore%20是将%20int%20存储到局部变量。iload%20从局部变量加载%20int%20值。iinc%20通过下标递增局部变量。ireturn%20从操作数堆栈中返回%20int%20类型的值。astore%20将引用存储到局部变量中。

    有了这些信息之后,来翻译一下上面的字节码内容:

    0%20iconst_1%20%20%20在操作数栈中存储数值%2011%20istore_0%20%20%20将操作数栈中的数据存储在局部变量的位置%2002%20iload_0%20%20%20%20从局部变量读取值到操作数栈3%20istore_1%20%20%20将操作数栈中存储%201%20存储在局部变量的位置%2014%20iinc%200%20by%201%20把局部变量位置%200%20的元素进行递增(+1)操作7%20iload_1%20将局部位置%201%20的值加载到操作数栈中8%20ireturn%20返回操作数栈中的%20int%20

    通过以上信息也许并不能直观的看出此方法的内部执行过程,可以通过方法执行流程图:try-catch-finally使用中的问题总结 - 图6通过以上图片可以看出:在%20finally%20语句(iinc%200,%201)执行之前,本地变量表中存储了两个信息,位置%200%20和位置%201%20都存储了一个值为%201%20的%20int%20值。而在执行%20finally(iinc%200,%201)之前只把位置%200%20的值进行了累加,之后又将位置%201%20的值(1)返回给了操作数栈,所以当执行返回操作(ireturn)时会从操作数栈中读到返回值为%201%20的结果,因此最终的执行是%201%20而不是%202。

    ③%20解决方案

    关于%20Java%20虚拟机是如何编译%20finally%20语句块的问题,有兴趣的读者可以参考《The%20JavaTM%20Virtual%20Machine%20Specification,%20Second%20Edition》中%207.13%20节%20Compiling%20finally。那里详细介绍了%20Java%20虚拟机是如何编译%20finally%20语句块。实际上,Java%20虚拟机会把%20finally%20语句块作为%20subroutine(对于这个%20subroutine%20不知该如何翻译为好,干脆就不翻译了,免得产生歧义和误解)直接插入到%20try%20语句块或者%20catch%20语句块的控制转移语句之前。但是,还有另外一个不可忽视的因素,那就是在执行%20subroutine(也就是%20finally%20语句块)之前,try%20或者%20catch%20语句块会保留其返回值到本地变量表(Local%20Variable%20Table)中,待%20subroutine%20执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过%20return%20或者%20throw%20语句将其返回给该方法的调用者(invoker)。因此如果在%20try-catch-finally%20中如果有%20return%20操作,一定要确保%20return%20语句只在方法的尾部出现一次!这样就能保证%20try-catch-finally%20中所有操作代码都会生效。

    ④%20正例代码private%20static%20int%20getValueByAmend()%20{%20%20%20%20int%20num%20=%201;%20%20%20%20try%20{%20%20%20%20%20%20%20%20//%20do%20something%20%20%20%20}%20catch%20(Exception%20e)%20{%20%20%20%20%20%20%20%20//%20do%20something%20%20%20%20}%20finally%20{%20%20%20%20%20%20%20%20num++;%20%20%20%20}%20%20%20%20return%20num;}

    坑3:finally中的代码“非最后”执行

    ①%20反例代码public%20static%20void%20main(String[]%20args)%20throws%20FileNotFoundException%20{%20%20%20%20execErr();}private%20static%20void%20execErr()%20{%20%20%20%20try%20{%20%20%20%20%20%20%20%20throw%20new%20RuntimeException();%20%20%20%20}%20catch%20(RuntimeException%20e)%20{%20%20%20%20%20%20%20%20e.printStackTrace();%20%20%20%20}%20finally%20{%20%20%20%20%20%20%20%20System.out.println("执行%20finally.");%20%20%20%20}}

    以上代码的执行结果如下:从以上结果可以看出%20finally%20中的代码并不是最后执行的,而是在%20catch%20打印异常之前执行的,这是为什么呢?

    ②%20原因分析

    产生以上问题的真实原因其实并不是因为%20try-catch-finally,当打开%20e.printStackTrace%20的源码就能看出一些端倪了,源码如下:从上图可以看出,当执行 e.printStackTrace() 和 finally 输出信息时,使用的并不是同一个对象。finally 使用的是标准输出流:**System.out**,而 **e.printStackTrace()** 使用的却是标准错误输出流:**System.err.println**,它们执行的效果等同于:

    1. public static void main(String[] args) {
    2. System.out.println("我是标准输出流");
    3. System.err.println("我是标准错误输出流");
    4. }

    而以上代码执行结果的顺序也是随机的,而产生这一切的原因,或许可以通过标准错误输出流(System.err)的注释和说明文档中看出:简单的对以上的注释做一个简单的翻译:

    “标准”错误输出流。该流已经打开,并准备接受输出数据。通常,此流对应于主机环境或用户指定的显示输出或另一个输出目标。按照惯例,即使主要输出流(out%20输出流)已重定向到文件或其他目标位置,该输出流(err%20输出流)也能用于显示错误消息或其他信息,这些信息应引起用户的立即注意。

    从源码的注释信息可以看出,标准错误输出流(System.err)和标准输出流(System.out)使用的是不同的流对象,即使标准输出流并定位到其他的文件,也不会影响到标准错误输出流。那么就可以大胆的猜测:二者是独立执行的,并且为了更高效的输出流信息,二者在执行时是并行执行的,因此看到的结果是打印顺序总是随机的。为了验证此观点,将标准输出流重定向到某个文件,然后再来观察%20System.err%20能不能正常打印,实现代码如下:

    public%20static%20void%20main(String[]%20args)%20throws%20FileNotFoundException%20{%20%20%20%20//%20将标准输出流的信息定位到%20log.txt%20中%20%20%20%20System.setOut(new%20PrintStream(new%20FileOutputStream("log.txt")));%20%20%20%20System.out.println("我是标准输出流");%20%20%20%20System.err.println("我是标准错误输出流");}

    以上代码的执行结果如下:当程序执行完成之后,发现在项目的根目录出现了一个新的 log.txt 文件,打开此文件看到如下结果:从以上结果可以看出标准输出流和标准错误输出流是彼此独立执行的,且%20JVM%20为了高效的执行会让二者并行运行,所以最终看到的结果是%20finally%20在%20catch%20之前执行了。

    ③%20解决方案

    知道了原因,那么问题就好处理,只需要将%20try-catch-finally%20中的输出对象,改为统一的输出流对象就可以解决此问题了。

    ④%20正例代码private%20static%20void%20execErr()%20{%20%20%20%20try%20{%20%20%20%20%20%20%20%20throw%20new%20RuntimeException();%20%20%20%20}%20catch%20(RuntimeException%20e)%20{%20%20%20%20%20%20%20%20System.out.println(e);%20%20%20%20}%20finally%20{%20%20%20%20%20%20%20%20System.out.println("执行%20finally.");%20%20%20%20}}

    改成了统一的输出流对象之后,手工执行了%20n%20次,并没有发现任何问题。

    坑4:finally中的代码“不执行”

    finally%20中的代码一定会执行吗?如果是之前会毫不犹豫的说“是的”,但是现在可能会这样回答:正常情况下%20finally%20中的代码一定会执行的,但如果遇到特殊情况%20finally%20中的代码就不一定会执行了,比如下面这些情况:

    • 在%20try-catch%20语句中执行了%20System.exit;
    • 在%20try-catch%20语句中出现了死循环;
    • 在%20finally%20执行之前掉电或者%20JVM%20崩溃了。

    如果发生了以上任意一种情况,finally%20中的代码就不会执行了。虽然感觉这一条有点“抬杠”的嫌疑,墨菲定律中,如果一件事有可能会发生,那么他就一定会发生。所以从严谨的角度来说,这个观点还是成立的,尤其是对于新手来说,神不知鬼不觉的写出一个自己发现不了的死循环是一件很容易的事。

    ①%20反例代码public%20static%20void%20main(String[]%20args)%20{%20%20%20%20noFinally();}private%20static%20void%20noFinally()%20{%20%20%20%20try%20{%20%20%20%20%20%20%20%20System.out.println("我是%20try~");%20%20%20%20%20%20%20%20System.exit(0);%20%20%20%20}%20catch%20(Exception%20e)%20{%20%20%20%20%20%20%20%20//%20do%20something%20%20%20%20}%20finally%20{%20%20%20%20%20%20%20%20System.out.println("我是%20fially~");%20%20%20%20}}

    以上代码的执行结果如下:从以上结果可以看出 finally 中的代码并没有执行。

    ② 解决方案

    排除掉代码中的 System.exit 代码,除非是业务需要,但也要注意如果在 try-cacth 中出现了 System.exit 的代码,那么 finally 中的代码将不会被执行。

    以太坊cppgolang区别 编程

    以太坊cppgolang区别

    以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
    progolang 编程

    progolang

    Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
    golangn个发送者 编程

    golangn个发送者

    Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
    golang技能图谱 编程

    golang技能图谱

    从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
    评论:0   参与:  5