try-catch-finally中的四大坑

目录

1.坑1:finally中使用return

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

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

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


在 Java 语言中 try-catch-finally 看似简单,但想要真正的“掌控”它,却并不是一件容易的事。这篇文章就拿 fianlly 来说吧,别看它的功能单一,但使用起来却没有那么简单,不信的话,来看下面的这几个例子...

1.坑1:finally中使用return

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

① 反例代码

public class Demo {
    public static void main(String[] args){
        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;
        }
    }

}

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

try-catch-finally中的四大坑_第1张图片

② 原因分析

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

③ 解决方案

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

④ 正例代码

public class Demo {
    public static void main(String[] args){
        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.坑2:finally中的代码好像“不执行”

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

① 反例代码

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

}

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

try-catch-finally中的四大坑_第2张图片

② 原因分析

本以为执行的结果会是 2,但万万没想到竟然是 1 ,有人可能会问:如果把代码换成 ++num,那么结果会不会是 2 呢?很抱歉的告诉你,并不会,执行的结果依然是 1。那为什么会这样呢?

这是因为JVM规范里面明确说明了这种情况:

If the try clause executes a return, the compiled code does the following:

1. Saves the return value (if any) in a local variable.
2. Executes a jsr to the code for the finally clause.
3. Upon return from the finally clause, returns the value saved in the local variable.

大意就是如果在try中return的情况下,先把try中将要return的值先存到一个本地变量中,即本例中的num=1将会被保存下来。接下来去执行finally语句,最后返回的是存在本地变量中的值,即num=1

try中有return, 会先将值暂存,无论finally语句中对该值做什么处理,最终返回的都是try语句中的暂存值。(当try和finally里都有return时,会忽略try的return,而使用finally的return.)​​​

③ 解决方案

实际上,Java 虚拟机会把 finally 语句块作为 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.坑3:finally中的代码“非最后”执行

① 反例代码

public class Demo {
    public static void main(String[] args) {
        error();
    }

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

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

try-catch-finally中的四大坑_第3张图片

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

② 原因分析

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

try-catch-finally中的四大坑_第4张图片

从上图可以看出,当执行 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)的注释和说明文档中看出:

try-catch-finally中的四大坑_第5张图片

try-catch-finally中的四大坑_第6张图片

做一个简单的翻译:

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

从源码的注释信息可以看出,标准错误输出流(System.err)和标准输出流(System.out)使用的是不同的流对象,即使标准输出流并定位到其他的文件,也不会影响到标准错误输出流。那么我们就可以大胆的猜测:二者是独立执行的,并且为了更高效的输出流信息,二者在执行时是并行执行的,因此我们看到的结果是打印顺序总是随机的(需要注意的是,上图运行结果两个输出是并行的,所以大多数结果还是finlly运行在后,少数情况finally在前,在此主要探讨finally在前)。

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

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;

public class Demo1 {
    public static void main(String[] args) throws FileNotFoundException {
        // 将标准输出流的信息定位到 log.txt 中
        System.setOut(new PrintStream(new FileOutputStream("D:\\java\\42009070204-zhao-likun\\java23\\JavatoGod\\JavatoGod\\Java_EE_first\\P06_blog_html\\target/log.txt")));
        System.out.println("我是标准输出流");
        System.err.println("我是标准错误输出流");
    }
}

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

try-catch-finally中的四大坑_第7张图片

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

try-catch-finally中的四大坑_第8张图片

从以上结果可以看出标准输出流和标准错误输出流是可以彼此独立执行的,且 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.坑4:finally中的代码真的“不执行”

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

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

如果发生了以上任意一种情况,finally 中的代码就不会执行了。虽然感觉这一条有点“抬杠”的嫌疑,但墨菲定律告诉我们,如果一件事有可能会发生,那么他就一定会发生。

① 反例代码

public class Demo2 {
    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("我是 finally~");
        }
    }

}

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

try-catch-finally中的四大坑_第9张图片

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

② 原因分析

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

③ 解决方案

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


总结

本文我们展示了 try-catch-finally 中存在的一些问题,有很实用的干货,也有一些看似“杠精”的示例,但这些都从侧面印证了一件事,那就是想使用好 try-catch-finally 并不是一件简单的事。最后,在强调一点,如果 try-catch-finally 中存在 return 返回值的操作,那么一定要确保 return 语句只在方法的尾部出现一次!最后,大家一起加油吧

你可能感兴趣的:(java,try-catch,finally,jvm)