finally 区域内的代码块在 return 之前被执行
由于 Java 程序中,所有的对象都是在堆上( Heap
)分配存储空间的,这些空间完全由垃圾回收机制来对它们进行管理。因此,从这一点可以分析得出一个推论: Java 中的异常处理模型的实现,其实要比
C++ 异常处理模型简单得多。例如,它首先不需要像 C++
异常处理模型中那样,必须要跟踪栈上的每一个“对象”的构造和析构过程(只有跟踪并掌握了这些信息,发生异常时, C++
系统它才会知道当前应该析构销毁哪些对象呀!),这是因为 Java 程序中,栈上是绝对没有“对象”的(实际只是对堆上对象的引用)。另外,还有
Java 语言中的异常对象的传递也更为简单和容易了,它只需传递一个引用指针而已,而完全不用考虑异常对象的构造、复制和销毁过程。
当然, Java 异常处理模型较 C++ 异常处理模型复杂的地方是,它引入了 finally 机制(主要用于数据库连接的关闭、
Socket 关闭、文件流的关闭等)。其实,我们也知道 finally 语法最早是在微软的 SEH
所设计出的一种机制,虽然它功能很强大,但是实现起来却并不是很难,从表象上来理解:当代码在执行过程中,遭遇到 return 和 goto
等类似的语句所引发作用域(代码执行流)转移时,便会产生一个局部展开( Local Unwinding );而由于异常而导致的 finally
块被执行的过程,往往被称为全局展开( Global Unwinding )。由于展开( Unwinding )而导致的 finally
块被执行的过程,非常类似于一个子函数(或子过程)被调用的过程。例如,当在 try 块中最后一条语句 return
被执行到的时候,一个展开操作便发生了,可以把展开操作想象成,是编译器在 return 语句之前插入了一些代码(这些代码完成对 finally
块的调用),因此可以得出结论: finally 区域内的代码块,肯定是在 return 之前被执行。
但是,请特别注意, finally 块区域中的代码虽然在 return 语句之前被执行,但是 finally 块区域中的代码是不能够通过重新赋值的方式来改变 return 语句的返回值。请看如下的示例代码:
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
// 你认为 test 函数返回的值是多少呢?
System.out.println(\"test 的返回值为: \" + test());
}
public static int test()
{
int ret = 1;
try
{
System.out.println(\"in try block\");
// 是返回 1 ,还是返回 2 呢?
return (ret);
}
catch(Exception e)
{
System.out.println(\"in catch block\");
e.printStackTrace();
}
finally
{
// 注意,这里重新改变了 ret 的值。
ret = 2;
System.out.println(\"in finally block!\");
}
return 0;
}
}
上面的示例程序中,本来是想在 finally 区域中通过改变 ret 的值,来影响 test 函数最终 return 的值。但是真的影响了吗?否!否!否!不信,看看运行结果吧!
in try block
in finally block!
test 的返回值为: 1
其实,在 SEH 异常处理模型中, try-finally 语句对此情况也是有相同结果的处理结果,同样是上面的那个程序,把它改称 C 语言的形式,用 VC 编译运行一把,验证一下结果,代码如下:
#include \"stdio.h\"
int test()
{
int ret = 1;
__try
{
printf(\"in try block
\");
return ret;
}
__finally
{
ret = 2;
printf(\"in finally block!
\");
}
return 0;
}
void main()
{
printf(\"test 的返回值为: %d
\", test());
}
通过调试可以很容易看出,在 return ret 之前,编译器实际上对 ret 赋值了一份临时变量,为的就是防止 finally
区域中的代码对这个 ret 值的改变。当然这只是 debug 版本的情况,实际上,在 release 版本, return ret
语句直接被编译器编译成 mov eax, 1 指令。
所以说,无论是在 SEH 异常处理模型中,还是 Java 的异常处理模型中, finally 块区域中的代码都是不能够通过重新赋值的方式来改变 try 区域中 return 语句的返回值。
强烈建议不要在 finally 内部使用 return 语句
上面刚刚说了, finally 块区域中的代码不会轻易影响 try 区域中 return 语句的返回值,但是有一种情况例外,那就是在 finally 内部使用 return 语句。示例程序如下:
// 示例程序 1 , Java 程序
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
System.out.println(\"test 的返回值为: \" + test());
}
public static int test()
{
int ret = 1;
try
{
System.out.println(\"in try block\");
return (ret);
}
catch(Exception e)
{
System.out.println(\"in catch block\");
e.printStackTrace();
}
finally
{
ret = 2;
System.out.println(\"in finally block!\");
// 这里添加了一条 return 语句
return ret;
}
}
}
// 示例程序 2 , C 程序
#include \"stdio.h\"
int test()
{
int ret = 1;
__try
{
printf(\"in try block
\");
return ret;
}
__finally
{
ret = 2;
printf(\"in finally block!
\");
return ret;
}
printf(\" 多余的
\");
return 0;
}
void main()
{
printf(\"test 的返回值为: %d
\", test());
}
上面的程序运行结果如下:
in try block
in finally block!
test 的返回值为: 2
也许大多数朋友都估计到,上面的 test 函数返回值是 2 。也即是说, finally 内部使用 return
语句后,它影响(覆盖了) try 区域中 return
语句的返回值。这真是一种特别糟糕的情况,虽然它表面上看起来不是那么严重,但是这种程序极易给它人造成误解(使得阅读该代码的人总得担心或考虑,是否有
其它地方影响了这里的 return 的返回值)。
之所以出现这种现象的真正原因是,由于 finally 区域中的代码先于 return 语句( try
作用域中的)被执行,但是,如果此时在 finally 内部也有一个 return 语句,这将会导致该函数直接就返回了,而致使 try
作用域中的 return 语句再也得不到执行机会(实际就是无效代码,被覆盖了)。
面对上述情况,其实更合理的做法是,既不在 try block 内部中使用 return 语句,也不在 finally 内部使用
return 语句,而应该在 finally 语句之后使用 return 来表示函数的结束和返回,把上面的程序改造一下,代码如下
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
System.out.println(\"test 的返回值为: \" + test());
}
catch(Exception e)
{
e.printStackTrace();
}
}
public static int test() throws RuntimeException
{
int ret = 1;
try
{
System.out.println(\"in try block\");
}
catch(RuntimeException e)
{
System.out.println(\"in catch block\");
e.printStackTrace();
throw e;
}
finally
{
ret = 2;
System.out.println(\"in finally block!\");
}
// 把 return 语句放在最后,这最为妥当
return ret;
}
}
另一种更糟糕的情况
上面刚刚讲到, finally 内部使用 return 语句会覆盖 try 区域中 return 语句的返回值。不仅与此, finally
内部使用 return 语句还会导致出现另外一种更为糟糕的局面。到底是何种糟糕情况呢?还是先看看下面的示例程序再说吧!代码如下:
import java.io.*;
public class Trans
{
public static void main(String[] args)
{
try
{
System.out.println(\"test 的返回值为: \" + test());
}
catch(Exception e)
{
e.printStackTrace();
}
}
public static int test() throws RuntimeException
{
int ret = 0;
try
{
System.out.println(\"in try block\");
// 这里会导致出现一个运行态异常
int i=4,j=0;
ret = i/j;
}
catch(RuntimeException e)
{
System.out.println(\"in catch block\");
e.printStackTrace();
// 异常被重新抛出,上层函数可以进一步处理此异常
throw e;
}
finally
{
System.out.println(\"in finally block!\");
// 注意,这里有一个 return 语句
return ret;
}
}
}
是不是觉得上面示例程序中的代码写的挺好的,挺简洁的,还挺严谨的,应该不会有什么 BUG
!阿愚告诉你,错了!绝对错了!而且问题很严重!要不,朋友们在编译运行此程序前,先预期一下它的运行结果,大家是不是觉得运行流程应该是这样子的:
首先在终端输出“ in try block ”,接着,由于程序运行时出现了一个被 0 处的异常( ArithmeticException
);于是,进入到 catch block 中,这里的代码将继续向终端输出了“ in catch block
”信息,以及输出异常的堆栈信息等;接着,由于异常在 catch block 中又被重新抛出了,所以控制权返回到 main 函数的 catch
block 中;对了,补充一点,也许大家会觉得,由于异常的 rethrow ,使得控制权离开 test 函数作用域的时候, finally
内的代码会被执行,也即“ in finally block ”信息也会被打印到终端上了 。仅仅如此吗?不妨看一下实际的运行结果,如下:
in try block
in catch block
java.lang.ArithmeticException: / by zero
at Trans.test(Trans.java:27)
at Trans.main(Trans.java:10)
in finally block!
test 的返回值为: 0
看了实际的运行结果,是不是觉得大吃一惊!被重新抛出( rethrow )的异常居然“丢弃了”,也即 main 函数中,并没有捕获到
test 函数中的任何异常,这也许大大出乎当初写这段代码的程序员的预料吧!究竟原因何在呢?其实, 罪魁祸首就是 finally block
内部的那条 return 语句 。因为这段程序的运行流程基本如刚才我们预期描述的那样,但是有一点是不对的,那就是当 test
函数中把异常重新抛出后,这将导致又一次的 catch 的搜索和匹配过程,以及 test 函数中的 UnWinding 操作,也即
finally 被调用执行,但是由于 finally 内部的 return 语句,不仅使得它结束了 test
函数的执行(并返回一个值给上层函数),而且这还使得上面的那个对异常进行进一步的操作过程给终止了(也即控制权进入到 catch block
的过程)。瞧瞧!后果严重吧!
是呀!如果 Java 程序员不注意这种问题,养成一个严谨的、好的编程习惯,它将会导致实际的许多 Java
应用系统中出现一些莫名奇妙的现象(总感觉系统中出现了某类异常,有一些问题,但上层的模块中却总是捕获不到相关的异常,感觉一些良好!其实不然,它完全
是由于 finally 块中的 return 语句不小心把异常给屏蔽丢弃掉了)。
总结
• finally 区域内的代码总在 return 之前被执行;
• 强烈建议不要在 finally 内部使用 return 语句。它不仅会影响函数的正确返回值,而且它可能还会导致一些异常处理过程的意外终止,最终导致某些异常的丢失。