try-catch-finally中的4个巨坑,老程序员也搞不定!

作者 | 王磊

来源 | Java中文社群(ID:javacn666)

转载请联系授权(微信ID:GG_Stone)

在 Java 语言中 try-catch-finally 看似简单,一副人畜无害的样子,但想要真正的“掌控”它,却并不是一件容易的事。别的不说,咱就拿 fianlly 来说吧,别看它的功能单一,但使用起来却“暗藏杀机”,若您不信,咱来看下面的这几个例子...

坑1:finally中使用return

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

① 反例代码

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

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

② 原因分析

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

③ 解决方案

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

④ 正例代码

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

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

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

① 反例代码

public static void main(String[] args) throws FileNotFoundException {System.out.println("执行结果:" + getValue());
}
private static int getValue() {int num = 1;try {return num;} finally {num++;}
}

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

② 原因分析

本以为执行的结果会是 2,但万万没想到竟然是 1,用马大师的话来讲:「我大意了啊,没有闪」。

有人可能会问:如果把代码换成 ++num,那么结果会不会是 2 呢?

很抱歉的告诉你,并不会,执行的结果依然是 1。那为什么会这样呢?想要真正的搞懂它,我们就得从这段代码的字节码说起了。

以上代码最终生成的字节码如下:

// class version 52.0 (52)
// access flags 0x21
public class com/example/basic/FinallyExample {// compiled from: FinallyExample.java// access flags 0x1public <init>()VL0LINENUMBER 5 L0ALOAD 0INVOKESPECIAL java/lang/Object.<init> ()VRETURNL1LOCALVARIABLE this Lcom/example/basic/FinallyExample; L0 L1 0MAXSTACK = 1MAXLOCALS = 1// access flags 0x9public static main([Ljava/lang/String;)V throws java/io/FileNotFoundException L0LINENUMBER 13 L0GETSTATIC java/lang/System.out : Ljava/io/PrintStream;NEW java/lang/StringBuilderDUPINVOKESPECIAL java/lang/StringBuilder.<init> ()VLDC "\u6267\u884c\u7ed3\u679c:"INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;INVOKESTATIC com/example/basic/FinallyExample.getValue ()IINVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)VL1LINENUMBER 14 L1RETURNL2LOCALVARIABLE args [Ljava/lang/String; L0 L2 0MAXSTACK = 3MAXLOCALS = 1// access flags 0xAprivate static getValue()ITRYCATCHBLOCK L0 L1 L2 nullL3LINENUMBER 18 L3ICONST_1ISTORE 0L0LINENUMBER 20 L0ILOAD 0ISTORE 1L1LINENUMBER 22 L1IINC 0 1L4LINENUMBER 20 L4ILOAD 1IRETURNL2LINENUMBER 22 L2FRAME FULL [I] [java/lang/Throwable]ASTORE 2IINC 0 1L5LINENUMBER 23 L5ALOAD 2ATHROWL6LOCALVARIABLE num I L0 L6 0MAXSTACK = 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 将引用存储到局部变量中。

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

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

通过以上信息也许你并不能直观的看出此方法的内部执行过程,没关系磊哥给你准备了方法执行流程图:

通过以上图片我们可以看出:在 finally 语句(iinc 0, 1)执行之前,本地变量表中存储了两个信息,位置 0 和位置 1 都存储了一个值为 1 的 int 值。而在执行 finally(iinc 0, 1)之前只把位置 0 的值进行了累加,之后又将位置 1 的值(1)返回给了操作数栈,所以当执行返回操作(ireturn)时会从操作数栈中读到返回值为 1 的结果,因此最终的执行是 1 而不是 2。

③ 解决方案

关于 Java 虚拟机是如何编译 finally 语句块的问题,有兴趣的读者可以参考《The JavaTM Virtual Machine Specification, Second Edition》中 7.13 节 Compiling finally。那里详细介绍了 Java 虚拟机是如何编译 finally 语句块。

实际上,Java 虚拟机会把 finally 语句块作为 subroutine(对于这个 subroutine 不知该如何翻译为好,干脆就不翻译了,免得产生歧义和误解)直接插入到 try 语句块或者 catch 语句块的控制转移语句之前。但是,还有另外一个不可忽视的因素,那就是在执行 subroutine(也就是 finally 语句块)之前,try 或者 catch 语句块会保留其返回值到本地变量表(Local Variable Table)中,待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)。

因此如果在 try-catch-finally 中如果有 return 操作,**一定要确保 return 语句只在方法的尾部出现一次!**这样就能保证 try-catch-finally 中所有操作代码都会生效。

④ 正例代码

private static int getValueByAmend() {int num = 1;try {// do something} catch (Exception e) {// do something} finally {num++;}return num;
}

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

① 反例代码

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

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

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

② 原因分析

产生以上问题的真实原因其实并不是因为 try-catch-finally,当我们打开 e.printStackTrace 的源码就能看出一些端倪了,源码如下:

从上图可以看出,当执行 e.printStackTrace()  和 finally 输出信息时,使用的并不是同一个对象。finally 使用的是标准输出流:System.out,而 e.printStackTrace()  使用的却是标准错误输出流:System.err.println,它们执行的效果等同于:

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

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

我们简单的对以上的注释做一个简单的翻译:

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

从源码的注释信息可以看出,标准错误输出流(System.err)和标准输出流(System.out)使用的是不同的流对象,即使标准输出流并定位到其他的文件,也不会影响到标准错误输出流。那么我们就可以大胆的猜测:二者是独立执行的,并且为了更高效的输出流信息,二者在执行时是并行执行的,因此我们看到的结果是打印顺序总是随机的。

为了验证此观点,我们将标准输出流重定向到某个文件,然后再来观察 System.err 能不能正常打印,实现代码如下:

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

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

当程序执行完成之后,我们发现在项目的根目录出现了一个新的 log.txt 文件,打开此文件看到如下结果:

从以上结果可以看出标准输出流和标准错误输出流是彼此独立执行的,且 JVM 为了高效的执行会让二者并行运行,所以最终我们看到的结果是 finally 在 catch 之前执行了。

③ 解决方案

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

④ 正例代码

private static void execErr() {try {throw new RuntimeException();} catch (RuntimeException e) {System.out.println(e);} finally {System.out.println("执行 finally.");}
}

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

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

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

  • 在 try-catch 语句中执行了 System.exit;

  • 在 try-catch 语句中出现了死循环;

  • 在 finally 执行之前掉电或者 JVM 崩溃了。

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

① 反例代码

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

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

从以上结果可以看出 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


往期推荐

final的8个小细节,听说只有高手才知道!你知道几个?


Java中的Switch都支持String了,为什么不支持long?


对象复制的7种方法,还是Spring的最好用!


关注我,每天陪你进步一点点!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/545006.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CentOS7安装Python3.4 ,让Python2和3共存

为什么80%的码农都做不了架构师&#xff1f;>>> #CentOS7安装Python3.4 &#xff0c;让Python2和3共存 环境&#xff1a;CentOS7.1 需求&#xff1a;网络畅通 编译需要的一些包&#xff0c;酌情安装 yum groupinstall "Development tools" yum install z…

字节二面:优化 HTTPS 的手段,你知道几个?

由裸数据传输的 HTTP 协议转成加密数据传输的 HTTPS 协议&#xff0c;给应用数据套了个「保护伞」&#xff0c;提高安全性的同时也带来了性能消耗。因为 HTTPS 相比 HTTP 协议多一个 TLS 协议握手过程&#xff0c;目的是为了通过非对称加密握手协商或者交换出对称加密密钥&…

js使用location的方法实验

location.hostname 返回 web 主机的域名 127.0.0.1 location.port 返回 web 主机的端口 &#xff08;80 或 443&#xff09; 8020 location.protocol 返回所使用的 web 协议&#xff08;http:// 或 https://&#xff09; http: location.pathname 返回当前页面的路径和文件名…

求首位相连一维数组最大子数组的和

结对成员&#xff1a; 朱少辉&#xff1a;主要负责代码编写 侯涛亮&#xff1a;主要负责程序测试 题目:一个首尾相接的一维整型数组&#xff0c;其中有正有负&#xff0c;求它的最大子数组并返回它的位置。 思路&#xff1a;在求一维子数组的基础上&#xff0c;先输入一个含有N…

SpringBoot接口幂等性实现的4种方案!

作者 | 超级小豆丁来源 | www.mydlq.club/article/94目录什么是幂等性什么是接口幂等性为什么需要实现幂等性引入幂等性后对系统的影响Restful API 接口的幂等性如何实现幂等性方案一&#xff1a;数据库唯一主键方案二&#xff1a;数据库乐观锁方案三&#xff1a;防重 Token 令…

scala 数组合并_Scala程序合并两个数组或数组缓冲区

scala 数组合并Scala | 合并两个数组 (Scala | Merging two arrays) Arrays are important data structures in programming and there may arise times when we have two different arrays and we need to merge them into one for processing. This is the case when you nee…

js的navigator对象的使用(浏览器信息)

window.navigator 对象包含有关访问者浏览器的信息。 <!DOCTYPE html> <html> <body> <div id"example"></div><script>txt "<p>Browser CodeName: " navigator.appCodeName "</p>"; txt &…

http中指定使用worker/prefork

注意&#xff1a; 2.4之前版本默认为prefork&#xff0c; 2.4已经变为event模式。三种模式比较&#xff1a; http://www.cnblogs.com/fnng/archive/2012/11/20/2779977.html在编译apache的时候&#xff0c;有一个参数叫做 --with-mpm... 等号后边用于指定那种模式&#xff…

Redis为什么变慢了?一文详解Redis性能问题 | 万字长文

Redis 作为优秀的内存数据库&#xff0c;其拥有非常高的性能&#xff0c;单个实例的 OPS 能够达到 10W 左右。但也正因此如此&#xff0c;当我们在使用 Redis 时&#xff0c;如果发现操作延迟变大的情况&#xff0c;就会与我们的预期不符。你也许或多或少地&#xff0c;也遇到过…

Java PipedOutputStream flush()方法与示例

PipedOutputStream类flush()方法 (PipedOutputStream Class flush() method) flush() method is available in java.io package. flush()方法在java.io包中可用。 flush() method is used to flush this PipedOutputStream and forces bytes to be written out of any buffered…

蜕变成蝶~Linux设备驱动之字符设备驱动

一、linux系统将设备分为3类&#xff1a;字符设备、块设备、网络设备。使用驱动程序&#xff1a; 字符设备&#xff1a;是指只能一个字节一个字节读写的设备&#xff0c;不能随机读取设备内存中的某一数据&#xff0c;读取数据需要按照先后数据。字符设备是面向流的设备&#x…

Android 手机发送短信

参照网上的例子&#xff0c;做了一个手机发送短信的app。 有两种发送方法&#xff1a;第一种方法测试没有信息的发送记录&#xff0c;第二种调用的sms的Activity还要自行操作。 首先是xml布局文件代码&#xff1a; <LinearLayout xmlns:android"http://schemas.andro…

感动哭了!《Java 编程思想》最新中文版开源!

前言还记得这本书吗&#xff1f;是不是已经在你的桌上铺满厚厚的一层灰了&#xff1f;随着 Java 8 的出现&#xff0c;这门语言在许多地方发生了翻天覆地的变化。最新版已经出来了&#xff0c;在新的版本中&#xff0c;代码的运用和实现上与以往不尽相同。本书可作为编程入门书…

Java Process getOutputStream()方法与示例

流程类的getOutputStream()方法 (Process Class getOutputStream() method) getOutputStream() method is available in java.lang package. getOutputStream()方法在java.lang包中可用。 getOutputStream() method is used to get the output stream of the process and sub-p…

android中requestFocus 以及与setFocusable的区别

<requestFocus /> 标签用于指定屏幕内的焦点View。 例如我们点击tab键或enter键焦点自动进入下一个输入框 用法: 将标签置于Views标签内部 <span style"font-size:14px;"> <EditText id"id/text"android:layout_width"…

韩信大招:一致性哈希

作者 | 悟空聊架构来源 | 悟空聊架构韩信点兵的成语来源淮安民间传说。常与多多益善搭配。寓意越多越好。我们来看下主公刘邦和韩信大将军的对话。刘邦&#xff1a;“你觉得我可以带兵多少&#xff1f;”韩信&#xff1a;“最多十万。”刘邦不解的问&#xff1a;“那你呢&#…

mysql连接非常慢的觖决办法及其它常见问题解决办法

2019独角兽企业重金招聘Python工程师标准>>> 编辑/etc/mysql/my.cnf 在[mysqld]段中加入 skip-name-resolve 重启mysql 禁用DNS反响解析&#xff0c;就能大大加快MySQL连接的速度。 转载于:https://my.oschina.net/ydsakyclguozi/blog/401768

Java SimpleTimeZone toString()方法与示例

SimpleTimeZone类toString()方法 (SimpleTimeZone Class toString() method) toString() method is available in java.util package. toString()方法在java.util包中可用。 toString() method is used for string denotation of this SimpleTimeZone. toString()方法用于此Sim…

最常见的10种Java异常问题!

封面&#xff1a;洛小汐译者&#xff1a;潘潘前言本文总结了有关Java异常的十大常见问题。目录检查型异常&#xff08;checked&#xff09; vs. 非检查型异常&#xff08;Unchecked&#xff09;异常管理的最佳实践箴言为什么在try代码块中声明的变量不能在catch或者finally中被…

OSSIM学习-英汉对照注释

Ossim的Web UI目前没有很好的本地化解决方案&#xff0c;这给不少初学Ossim的用户尤其是英文不太好的人来说&#xff0c;带来了一些麻烦&#xff0c;下面是部分英汉对照注释&#xff0c;并在不断完善中&#xff0c;希望对大家学习过程中&#xff0c;有所帮助。Action 动作Actio…