前言
- 在上一篇文章中,我们介绍了
Java
异常的基本概念,Throwable
、异常处理关键字:try-catch-finally、throw、throws
;本篇文章我们将更加深入的了解finally
在异常处理中的常见问题和底层原理。
版本
Java 8
finally
中的陷阱
- 我们知道无论是否发生异常还是
try 或 catch
中存在return
,finally
都会执行,下面我们来看看下面几种场景:
finally 中使用 return
- 当我们在
finally
中使用return
时,try
或catch
中的return
会失效或异常丢失(见下文),会在finally
直接返回。
public class Main {
public static void main(String[] args) {
System.out.println(extracted());
}
private static int extracted() {
int a = 1;
try {
a = 2;
a = a / 0;
return a;
} catch (Exception e) {
System.out.println(e);
return a;
} finally {
System.out.println("this is finally");
return -1;
}
}
}
// 输出 finally 中直接 return -1
java.lang.ArithmeticException: / by zero
this is finally
-1
finally
中修改数据的影响
- 如果你在
finally
代码块中修改了数据,你可能会有一些奇妙的体验。
基本类型
代码语言:java复制public class Main {
public static void main(String[] args) {
System.out.println(extracted());
}
private static int extracted() {
int a = 1;
try {
a = 2;
return a;
} finally {
System.out.println("this is finally");
a = 3;
}
}
}
// 输出
this is finally
2
- 我们可以得出结论在
finally
中修改基本类型不会影响try 、catch
中return
中的返回值(但是会影响finally
中的return
,见下面的案例)。
public class Main {
public static void main(String[] args) {
System.out.println(extracted());
}
private static int extracted() {
int a = 1;
try {
a = 2;
return a;
} finally {
System.out.println("this is finally");
a = 3;
return a;
}
}
}
// 输出
this is finally
5
引用类型
代码语言:java复制// 案例一
public class Main {
public static void main(String[] args) {
System.out.println(extracted());
}
private static Object extracted() {
Person person = new Person();
try {
return person;
} finally {
System.out.println("this is finally");
person.age = 5;
}
}
}
class Person {
int age;
@Override
public String toString() {
return "Person age= " age;
}
}
// try 中的 return 被修改
this is finally
Person age= 5
// 案例二
public class Main {
public static void main(String[] args) {
System.out.println(extracted());
}
private static Object extracted() {
Person person = new Person();
try {
return person;
} finally {
System.out.println("this is finally");
person = (new Person());
person.age = 3;
}
}
}
class Person {
int age;
@Override
public String toString() {
return "Person age= " age;
}
}
// try 中的 return 没有被修改
this is finally
Person age= 0
- 上面的结果看着有点奇怪但实际上很好理解,我们在以前的文章中讲过,
Java 实际上只有值传递而不存在引用传递
,当为返回值为引用类型时,返回的其实是一个地址,在案例一中我们使用地址修改了原内容,而在案例二中,我们其实将 person 指向了新的地址(new Person()),因此并没有修改原返回值地址的内容。
finally
中的代码 “非最后” 执行
- 有时候我们发现
finally
中的代码 “非最后” 执行,那么有可能是并行执行了,比如:
public class Main {
public static void main(String[] args) {
extracted();
}
private static void extracted() {
try {
throw new IllegalStateException();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("this is finally");
}
}
}
// 比较难出现
this is finally
java.lang.IllegalStateException
at Main.extracted(Main.java:9)
at Main.main(Main.java:4)
- 实际上是因为
e.printStackTrace() 使用的是 System.err,而 System.out.println 使用的是 System.out
,标准输出流和标准错误输出流是彼此独立执行的,且JVM
为了高效的执行会让二者并行运行,所以会出现finally 中的代码 “非最后” 执行的场景。
finally
代码块一定会执行?
- 虽然这里有一定抬杠的嫌疑,但实际上确实有一些场景下
finally
代码块不会执行,比如:
在 try-catch 语句中执行了 System.exit
在 try-catch 语句中出现了死循环
在 finally 执行之前 JVM 崩溃
- 在
try-catch
语句中执行了System.exit
public class Main {
public static void main(String[] args) {
extracted();
}
private static void extracted() {
try {
// 此代码块执行完程序退出
System.exit(0);
throw new IllegalStateException();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("this is finally");
}
}
}
异常丢失
- 如果我们在
finally
代码块中抛出异常或使用retrun
,将会导致我们try-catch
中的异常丢失。
// 案例一
public class Main {
public static void main(String[] args) throws Exception {
extracted();
}
private static void extracted() throws Exception {
try {
throw new IllegalStateException();
} finally {
throw new Exception("Exception");
}
}
}
// 输出
Exception in thread "main" java.lang.Exception: Exception
at Main.extracted(Main.java:11)
at Main.main(Main.java:4)
// 案例二
public class Main {
public static void main(String[] args) throws Exception {
extracted();
}
private static int extracted() throws Exception {
try {
throw new IllegalStateException();
} finally {
return 1;
}
}
}
finally 底层原理分析
- 《The JavaTM Virtual Machine Specification, Second Edition》 一书中我们可以知道 Java 虚拟机是如何编译 finally:
实际上,Java 虚拟机会把 finally 语句块作为 subroutine 直接插入到 try 语句块或者 catch 语句块的控制转移语句之前。还有另外一个不可忽视的因素,那就是在执行 subroutine(也就是 finally 语句块)之前,try 或者 catch 语句块会保留其返回值(基本类型值或地址)到本地变量表(Local Variable Table)中,待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)。
- 理解了
JVM
对finally
的实现,我们其实就很好理解finally 中修改数据的影响
中的案例,有兴趣的朋友可以下去深入了解。
总结
- 本文我们结合了
finally
在实际使用中可能出现的问题并进行分析对应的原因,最后介绍了finally
在JVM
中的实现原理,帮助我们在日常开发的更好的使用finally
,下篇文章将会介绍实际异常处理中的一些最佳实践。
个人简介