Java进阶知识7:try-catch-finally中的4个巨坑

前言:在 Java 语言中 try-catch-finally 看似简单,一副人畜无害的样子,但想要真正的掌握它,却并不是一件容易的事。别的不说,咱就拿 fianlly 来说:finally 中的代码一定会执行吗?如果是之前我会毫不犹豫的说“是的”,但在遭受了面试官的毒打之后,我会这样回答:正常情况下 finally 中的代码一定会执行的,但如果遇到特殊情况 finally 中的代码就不一定会执行了。如果 try-catch-finally 中存在 return 返回值的情况,一定要确保 return 语句只在方法的尾部出现一次。

感谢两位大佬的文章,致敬:

捡田螺的小男孩:try-catch-finally中的4个巨坑,老程序员也搞不定!

店小不二:【搞定面试官】try中有return,finally还会执行吗?

一、finally中使用return,会忽略try的return

JVM规范规定:当try和finally里都有return时,会忽略try的return,而使用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;
}

二、finally中的代码貌似“不执行”

JVM规范规定:try中有return, 会先将值暂存,无论finally语句中对该值做什么处理,最终返回的都是try语句中的暂存值。

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

① 反例代码

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。

② 解决方案

关于 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;
}

三、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("我是标准错误输出流");
}

而以上代码执行结果的顺序也是随机的,而产生这一切的原因是:

标准输出流和标准错误输出流是彼此独立执行的,且 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 次,并没有发现任何问题。


四、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 语句只在方法的尾部出现一次

  1. try中有return, 会先将值暂存,无论finally语句中对该值做什么处理,最终返回的都是try语句中的暂存值。

  2. 当try与finally语句中均有return语句,会忽略try中return。

  3. 正常情况下 finally 中的代码一定会执行的,但如果遇到特殊情况 finally 中的代码就不一定会执行了

猜你喜欢

转载自blog.csdn.net/CSDN2497242041/article/details/114992637