关于java:trycatchfinally中的4个大坑不小心就栽进去了

4次阅读

共计 6631 个字符,预计需要花费 17 分钟才能阅读完成。

在 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] 
    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

关注公众号「Java 中文社群」发现更多干货。
查看 Github 发现更多精彩:https://github.com/vipstone/a…

正文完
 0