常见的一个完整的try catch finally结构语句是这样的
try{}
catch(){}
finally{}
有一点基础的读者都会对finally有一点理解:
- 如果在try块,catch块中return,会执行完finally块语句后再返回
- 如果在try块,catch块中抛出了未受检查的异常,会执行完finally块语句后再抛出异常
基于这两点,如果程序到达try块,那么finally块的代码则一定会执行,所以finally块常用来安全的清理资源,锁释放。
但是finally还有一些细节需要小心,不然可能会得到你意想不到的结果
我们先上一段代码,请读者猜测命令行输出的结果
public class TestClass {
public int m1(){
int x;
try{
x = 1;
throw new Exception();
} catch (Exception e){
x = 2;
return x;
}finally {
x = 3;
}
}
public int[] m2(){
int[] x = new int[2];
try{
x[0] = 1;
throw new Exception();
} catch (Exception e){
x[1] = 2;
return x;
}finally {
x[1] = 3;
}
}
public String m3(){
String x = null;
try{
x = "1";
throw new Exception();
} catch (Exception e){
x = "2";
return x;
}finally {
x += "3";
}
}
public static void main(String[] args) {
TestClass tc = new TestClass();
System.out.println("m1():" + tc.m1());
System.out.println("m2()[1]:" + tc.m2()[1]);
System.out.println("m3():" + tc.m3());
}
}
最后的返回值是
m1():2
m2()[1]:3
m3():2
如果返回值和你预期的一样,那么说明你已经理解了我将要说的内容,可以不用继续往下看了
首先来看第一个方法
public int m1(){
int x;
try{
x = 1;
throw new Exception();
} catch (Exception e){
x = 2;
return x;
}finally {
x = 3;
}
}
或许有的读者预期的结果是
m1():3
不是说finally块一定会执行吗,为什么x的值没有发生变化,finally块确实执行了,但是为什么x还是等于2
这里就是我今天要说的第一点,当try块或者catch块中提前返回时,finally块的语句不会改变return的返回值
具体来看catch块和finally块的字节码,不想看字节码,想直接看结果的可以跳过这个部分
字节码分析 ↓↓
//catch块
// x=2
11: iconst_2
12: istore_1
13: iload_1
14: istore_3
//finally块
// x=3
15: iconst_3
16: istore_1
17: iload_3
//return
18: ireturn
为了便于理解,我先给出局部变量表1和3位置的含义(不准确,因为局部变量表是可以复用的,因此可能这个时候代表的是变量x下一个时刻代表y,但这个例子里局部变量表没有被复用)
下标 | 0 | 1 | 2 | 3 |
---|---|---|---|---|
含义 | int x | int returnValue |
字节码的部分我简单解释一下
第11、12的意思就是将数字2存到局部变量表1的位置
第13、14行的意思是从局部变量表中1的位置读取数字2,然后再将数字2存到局部变量表3的位置
此时局部变量表的数值为
下标 | 0 | 1(x) | 2 | 3(returnValue) |
---|---|---|---|---|
数值 | int 2 | int 2 |
第15、16行的意思就是将数字3存到局部变量表1的位置
此时局部变量表的数值为
下标 | 0 | 1(x) | 2 | 3(returnValue) |
---|---|---|---|---|
数值 | int 3 | int 2 |
第17、18行的意思就是读取局部变量表3位置的数字作为返回值返回
因此这里看出,当语句要执行finally块时,它先把准备返回的数值2临时保存在了3这个位置,当执行完finally块之后,回来把3位置的值给返回了
字节码分析 ↑↑
我分析一下过程:
- 首先程序捕获到异常,进入了catch块
- 执行到x=2之后,想继续执行return方法直接返回
- 这时候finally语句块说,你等等,我还有方法没执行,先不要返回,这时return语句想,虚拟机要求我必须执行finally,那好吧,我去执行finally语句块,我把准备返回的值2先存放在returnValue里面,等我执行完了再回来返回
- 执行完finally块
- return回来,把刚才暂放在returnValue的值2取出来返回
由上面可以看出,finally不管执行什么语句,到最后return总是返回已经提前准备好的returnValue,finally语句不会改变catch块中的返回值
接下来我们来看第二个方法的代码
public int[] m2(){
int[] x = new int[2];
try{
x[0] = 1;
throw new Exception();
} catch (Exception e){
x[1] = 2;
return x;
}finally {
x[1] = 3;
}
}
这里输出的x[1] 为什么又是3而不是2了呢,不是说finally不会改变return的返回值吗
m2()[1]:3
这里是我要说的第二点,JAVA中只有值传递,没有引用传递,对于引用类型(例如数组,对象),仍然也是值传递,只是这个值传递的是对象的地址
那我们继续按照刚才的思路,当return准备返回时,先去执行finally块语句,并且把准备返回的值保存在了retunValue里面
这时returnValue保存的是数组的首地址:0XABCDEF,最后返回值0XABCDEF时不变的,但是可以改变对象的状态,使数组int[1]的更改为3。
此时返回的仍然是0XABCDEF,但数组对象的状态已经被改变了。
接下来我们来看第三个方法的代码
public String m3(){
String x = null;
try{
x = "1";
throw new Exception();
} catch (Exception e){
x = "2";
return x;
}finally {
x += "3";
}
}
综合以上两点,有经验的读者可能已经明白了,为什么m3方法返回的是2而不是12
m3():2
这里就是我的说的第三点,不可变类型只能改变引用不能改变状态,所谓不可变类型不仅是用final修饰的类,还要求类的域是不可变的,对于String类
x += "3";
这一步会被分解为三个操作
- 获取x的值"1"
- 生成一个新的String对象,对象的值为"1" + "2"即"12"
- 将新的String对象的引用替换x的引用
在这个方法中,首先是会将x的地址保存到returnValue
而finally的语句x+= "3"会生成一个新的String对象,并将x的引用指向它
最后return返回的returnValue仍然是"2"这个字符串
总结
最后总结一下,当在
try{}
catch(){return}
finally{}
或
try{return}
catch(){}
finally{}
的结构中
- finally块的代码一定会执行
- finally块的代码不会改变try块catch块的返回值
- 如果try块catch块return的是引用类型的地址,finally块可以改变返回对象的状态