问题引出
课上讲到异常处理的finally语句时,讲到这么一个例子(我又简化了一点):
public static void foo() {
try {
System.out.println("1");
// 申请一些资源
// 可能抛出异常
System.out.println("2");
} catch (Exception e) {
System.out.println("3");
} finally {
System.out.println("4");
// 释放资源
}
System.out.println("5");
}
这个方法对可能存在的异常进行处理,并且及时释放资源。
当存在异常时,输出依次为:1 3 4 5
,也就是说try块在执行到抛出异常之后就不进行下去了,接着执行相应异常块的代码,然后再执行finally块的代码,最后执行整个try-catch-finally结构之后的语句。
我在这里有一个问题,既然整个try-catch-finally结构之后的代码还能进行,那为什么不在之后再进行资源释放呢?
比如这样子:
public static void foo() {
try {
System.out.println("1");
// 申请一些资源
// 可能抛出异常
System.out.println("2");
} catch (Exception e) {
System.out.println("3");
}
// 不使用finally来释放资源,依然可以释放try中的资源
System.out.println("5");
}
一个比较显然的原因自然是finally语句可以提高代码可读性。
但是仅仅为了提高可读性而创造finally语句还是显得太牵强了。一定还有其他更重要的原因。
问题结论
先说结论。
上面的想法忽略了流程控制语句的作用。如果流程控制语句提前结束了该方法,那么很可能造成资源未得到释放。
- finally块的作用在try、catch、finally块中没有超出块范围的控制语句时,并没有凸显出来。
- 一旦有诸如return、throw语句出现时,如果没有finally块或者没有在finally块当中进行资源释放,将会造成方法在没有释放资源的情况下结束。
- 因此,在finally块中完成对try块申请资源的释放成为了一种惯例。后续在修改try块的代码时也不容易因为控制语句出错。
执行流程
为了进一步探索try-catch-finally结构在有控制语句时的执行流程,做了以下尝试:
存在return时
示例1
return被阻塞:
public static void main(String[] args) {
foo();
}
public static void foo() {
try {
System.out.println("1");
return;
} finally {
System.out.println("2");
}
// 这里的代码会被IDE提示永不可达。
}
结果:
1
2
说明:try中的return被阻塞,接着执行了finally的语句,再返回。
示例2
看看哪个return先返回:
public static void main(String[] args) {
System.out.println("return: " + foo());
}
public static int foo() {
try {
System.out.println("1");
return 1;
} finally {
System.out.println("2");
return 2;
}
// 这里的代码会被IDE提示永不可达。
}
结果:
1
2
return: 2
说明:try块中的return依然先被阻塞,
先执行finally块中的语句,并且finally块
中的return被立即执行。
示例3
添加一个变量试试:
public static void main(String[] args) {
System.out.println("return: " + foo());
}
public static int foo() {
int i = 0;
try {
System.out.println("1");
return i;
} finally {
System.out.println("2");
i++;
System.out.println("i = " + i);
}
// 这里的代码会被IDE提示永不可达。
}
结果:
1
2
i = 1
return: 0
说明:这个结果比较有意思,流程依然同上,i也真的自
加1了,出乎意料的是,try当中的return语句返回的是0,
即自加前的i(=0),而不是自加后的i(=1)。
或许try中return被暂时阻塞的同时,对应的返回值也会被
临时存起来?我们检验一下这个猜想。
示例4
再添加一些操作试试:
public static void main(String[] args) {
System.out.println("return: " + foo());
}
public static int foo() {
int i = 10;
// 之前的初值0还是太特殊了,换成10
try {
System.out.println("1");
return i++;
} finally {
System.out.println("before finally:i = " + i);
System.out.println("2");
i += 5;
System.out.println("after finally:i = " + i);
}
// 这里的代码会被IDE提示永不可达。
}
结果:
1
before finally:i = 11
2
after finally:i = 16
return: 10
执行流程同上,按照之前的想法,i在try中自加前就
作为返回值(=10)被存储起来,接着i自加1,执行finally
中的语句,最终返回时,执行的依然是阻塞前的return,
返回的也是阻塞前的return对应的返回值10。
示例5
调换一下return时自加的顺序:
public static void main(String[] args) {
System.out.println("return: " + foo());
}
public static int foo() {
int i = 10;
// 之前的初值0还是太特殊了,换成10
try {
System.out.println("1");
return ++i;
} finally {
System.out.println("before finally:i = " + i);
System.out.println("2");
i += 5;
System.out.println("after finally:i = " + i);
}
// 这里的代码会被IDE提示永不可达。
}
结果:
1
before finally:i = 11
2
after finally:i = 16
return: 11
思路同上,但是i先自加1,再作为返回值被存储起来。
有异常抛出的情况
这里指的异常是指在try-catch-finally结构当中不会被捕获,导致方法终止的异常,故而这里依然先不考虑catch。
示例6
加个异常看看:
public static void main(String[] args) {
try {
foo();
} catch (Exception e) {
System.out.println("main:get the Exception.");
}
}
public static void foo() throws Exception{
try {
System.out.println("1");
throw new Exception();
} finally {
System.out.println("2");
}
// 这里的代码会被IDE提示永不可达。
}
结果:
1
2
main:get the Exception.
执行流程同示例1
示例7
看看从哪个Exception返回:
public static void main(String[] args) {
try {
foo();
} catch (Exception e) {
System.out.println("main:Exception: "
+ e.getMessage());
}
}
public static void foo() throws Exception{
try {
System.out.println("1");
throw new Exception("1");
} finally {
System.out.println("2");
throw new Exception("2");
}
// 这里的代码会被IDE提示永不可达。
}
结果:
1
2
main:Exception: 2
流程同示例2,异常由finally块中抛出并返回main方法。
示例8
添加一个变量试试:
public static void main(String[] args) {
try {
foo();
} catch (Exception e) {
System.out.println("main:Exception: "
+ e.getMessage());
}
}
public static void foo() throws Exception{
int i = 10;
try {
System.out.println("1");
throw new Exception("" + i);
} finally {
System.out.println("2");
i += 5;
}
// 这里的代码会被IDE提示永不可达。
}
结果:
1
2
main:Exception: 10
已经没什么新意了,这里异常信息保存的也是原始的i,
目前为止和return的情况都相同。
既有return又有异常
示例9
如果即有return又有异常,返回哪个呢?
public static void main(String[] args) {
try {
System.out.println("return: " + foo());
} catch (Exception e) {
System.out.println("Exception: "
+ e.getMessage());
}
}
public static int foo() throws Exception{
int i = 10;
try {
System.out.println("1");
throw new Exception("" + i);
} finally {
System.out.println("2");
i += 5;
System.out.println("i = " + i);
return i;
}
// 这里的代码会被IDE提示永不可达。
}
结果:
1
2
i = 15
return: 15
不出所料,try中的控制流语句会被阻塞,
谁在finally块,谁先被执行。这里就是
return执行返回了。
示例10
换个位置,证明一下:
public static void main(String[] args) {
try {
System.out.println("return: " + foo());
} catch (Exception e) {
System.out.println("Exception: "
+ e.getMessage());
}
}
public static int foo() throws Exception{
int i = 10;
try {
System.out.println("1");
return i;
} finally {
System.out.println("2");
i += 5;
System.out.println("i = " + i);
throw new Exception("" + i);
}
// 这里的代码会被IDE提示永不可达。
}
结果:
1
2
i = 15
Exception: 15
嗯,示例9猜想得没错。
结论
- try中的return和不可捕获的异常都会先被阻塞,接着执行finally中的语句;
- try中的return返回值以及不可捕获异常的异常信息都是在第一次遇到该语句时就确定的。后来的修改不会改变之,如果还能回到try中继续执行抛出异常或者返回的操作,那么对应的参数/返回值仍然是阻塞时的值;
- 如果finally当中也有抛出Exception或者return,那么程序会直接从finally中的Exception和return返回调用者,而不管try中的Exception和return。