百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

try-catch-finally中的4个大坑

ccwgpt 2024-12-19 10:17 115 浏览 0 评论

在 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 0x1
  public <init>()V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/example/basic/FinallyExample; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V throws java/io/FileNotFoundException 
   L0
    LINENUMBER 13 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "\u6267\u884c\u7ed3\u679c:"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKESTATIC com/example/basic/FinallyExample.getValue ()I
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 14 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 3
    MAXLOCALS = 1

  // access flags 0xA
  private static getValue()I
    TRYCATCHBLOCK L0 L1 L2 null
   L3
    LINENUMBER 18 L3
    ICONST_1
    ISTORE 0
   L0
    LINENUMBER 20 L0
    ILOAD 0
    ISTORE 1
   L1
    LINENUMBER 22 L1
    IINC 0 1
   L4
    LINENUMBER 20 L4
    ILOAD 1
    IRETURN
   L2
    LINENUMBER 22 L2
   FRAME FULL [I] [java/lang/Throwable]
    ASTORE 2
    IINC 0 1
   L5
    LINENUMBER 23 L5
    ALOAD 2
    ATHROW
   L6
    LOCALVARIABLE num I L0 L6 0
    MAXSTACK = 1
    MAXLOCALS = 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   在操作数栈中存储数值 1
 1 istore_0   将操作数栈中的数据存储在局部变量的位置 0
 2 iload_0    从局部变量读取值到操作数栈
 3 istore_1   将操作数栈中存储 1 存储在局部变量的位置 1
 4 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

相关推荐

团队管理“布阵术”:3招让你的团队战斗力爆表!

为何古代军队能够以一当十?为何现代企业有的团队高效似“特种部队”,有的却松散若“游击队”?**答案正隐匿于“布阵术”之中!**今时今日,让我们从古代兵法里萃取3个核心要义,助您塑造一支战斗力爆棚的...

知情人士回应字节大模型团队架构调整

【知情人士回应字节大模型团队架构调整】财联社2月21日电,针对原谷歌DeepMind副总裁吴永辉加入字节跳动后引发的团队调整问题,知情人士回应称:吴永辉博士主要负责AI基础研究探索工作,偏基础研究;A...

豆包大模型团队开源RLHF框架,训练吞吐量最高提升20倍

强化学习(RL)对大模型复杂推理能力提升有关键作用,但其复杂的计算流程对训练和部署也带来了巨大挑战。近日,字节跳动豆包大模型团队与香港大学联合提出HybridFlow。这是一个灵活高效的RL/RL...

创业团队如何设计股权架构及分配(创业团队如何设计股权架构及分配方案)

创业团队的股权架构设计,决定了公司在随后发展中呈现出的股权布局。如果最初的股权架构就存在先天不足,公司就很难顺利、稳定地成长起来。因此,创业之初,对股权设计应慎之又慎,避免留下巨大隐患和风险。两个人如...

消息称吴永辉入职后引发字节大模型团队架构大调整

2月21日,有消息称前谷歌大佬吴永辉加入字节跳动,并担任大模型团队Seed基础研究负责人后,引发了字节跳动大模型团队架构大调整。多名原本向朱文佳汇报的算法和技术负责人开始转向吴永辉汇报。简单来说,就是...

31页组织效能提升模型,经营管理团队搭建框架与权责定位

分享职场干货,提升能力!为职场精英打造个人知识体系,升职加薪!31页组织效能提升模型如何拿到分享的源文件:请您关注本头条号,然后私信本头条号“文米”2个字,按照操作流程,专人负责发送源文件给您。...

异形柱结构(异形柱结构技术规程)

下列关于混凝土异形柱结构设计的说法,其中何项正确?(A)混凝土异形柱框架结构可用于所有非抗震和抗震设防地区的一般居住建筑。(B)抗震设防烈度为6度时,对标准设防类(丙类)采用异形柱结构的建筑可不进行地...

职场干货:金字塔原理(金字塔原理实战篇)

金字塔原理的适用范围:金字塔原理适用于所有需要构建清晰逻辑框架的文章。第一篇:表达的逻辑。如何利用金字塔原理构建基本的金字塔结构受众(包括读者、听众、观众或学员)最容易理解的顺序:先了解主要的、抽象的...

底部剪力法(底部剪力法的基本原理)

某四层钢筋混凝土框架结构,计算简图如图1所示。抗震设防类别为丙类,抗震设防烈度为8度(0.2g),Ⅱ类场地,设计地震分组为第一组,第一自振周期T1=0.55s。一至四层的楼层侧向刚度依次为:K1=1...

结构等效重力荷载代表值(等效重力荷载系数)

某五层钢筋混凝土框架结构办公楼,房屋高度25.45m。抗震设防烈度8度,设防类别丙类,设计基本地震加速度0.2g,设计地震分组第二组,场地类别为Ⅱ类,混凝土强度等级C30。该结构平面和竖向均规则。假定...

体系结构已成昭告后世善莫大焉(体系构架是什么意思)

实践先行也理论已初步完成框架结构留余后人后世子孙俗话说前人栽树后人乘凉在夏商周大明大清民国共和前人栽树下吾之辈已完成结构体系又俗话说青出于蓝而胜于蓝各个时期任务不同吾辈探索框架结构体系经历有限肯定发展...

框架柱抗震构造要求(框架柱抗震设计)

某现浇钢筋混凝土框架-剪力墙结构高层办公楼,抗震设防烈度为8度(0.2g),场地类别为Ⅱ类,抗震等级:框架二级,剪力墙一级,混凝土强度等级:框架柱及剪力墙C50,框架梁及楼板C35,纵向钢筋及箍筋均采...

梁的刚度、挠度控制(钢梁挠度过大会引起什么原因)

某办公楼为现浇钢筋混凝土框架结构,r0=1.0,混凝土强度等级C35,纵向钢筋采用HRB400,箍筋采用HPB300。其二层(中间楼层)的局部平面图和次梁L-1的计算简图如图1~3(Z)所示,其中,K...

死要面子!有钱做大玻璃窗,却没有钱做“柱和梁”,不怕房塌吗?

活久见,有钱做2层落地大玻璃窗,却没有钱做“柱子和圈梁”,这样的农村自建房,安全吗?最近刷到个魔幻施工现场,如下图,这栋5开间的农村自建房,居然做了2个全景落地窗仔细观察,这2个落地窗还是飘窗,为了追...

不是承重墙,物业也不让拆?话说装修就一定要拆墙才行么

最近发现好多朋友装修时总想拆墙“爆改”空间,别以为只要避开承重墙就能随便砸!我家楼上邻居去年装修,拆了阳台矮墙想扩客厅,结果物业直接上门叫停。后来才知道,这种配重墙拆了会让阳台承重失衡,整栋楼都可能变...

取消回复欢迎 发表评论: