原题
try {} 里有一个 return 语句,那么紧跟在这个 try 后的 finally {}里的代码会不会被执行?什么时候被执行?在 return 前还是后?
乍一看题目很简单嘛,java 规范都说了,finally 会在 try 代码块的 return 之前执行,你这文章写得没意义,不看了
你等等!(拿起我身边的五尺砍刀)
神奇栗子
看完这个栗子,你在想想执行顺序到底是怎样的
栗子代码
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
int t = 0;
try {
return t;
} finally {
++t;
}
}
分析一下
test() 方法内,在 try 中 return 了 t,那么在 main 方法中 test() 函数的返回值应该是 t=0,即控制台输出 0
但是因为有 finally 的存在,而 finally 中对 t 进行了自增运算,并且 finally 会在 try 中的 return 语句之前执行,所以正确的情况是控制台输出1
所以你最终确定的答案是:控制台输出1
然而事实并非如此,将程序跑起来之后,得到的结果是:
输出0
将栗子跑起来亲眼看一下吧~
得到这个结果你也许要爆炸了,啥?java 规范说的都是错的?!
不用急,到我给sun洗地的时间了
洗地时间
在洗地之前,你很有必要先理解 java 中的值传递,如果你已经了解该内容可略过下面这一个小节点
java中的值传递
由于这只是本文内容引申出去的知识点,不过多赘述,随便唠两句,能借此明白则好,不明白希望借助搜索引擎明白一下!
在 java 的方法调用中,时常需要传递参数,那么传递的参数是将之前的变量直接传递给方法内了吗?
显然不是的,调用方法传递参数的时候,传递的只是原变量的一个副本(复制体),换句话说就是,将变量的值传递给了方法体,而并没有真正的将变量传递进去。
看个栗子:
public static void main(String[] args) {
int t = 0;
test(t);
System.out.println(t);
}
public static int test(int a) {
a = 111;
}
正确输出是0,因为 test() 方法内拿到的 a,只是 t 的一个副本(复制体)而并不直接是 t,test() 内改变了 a 的值,并不影响 t 的值
以上是对于基本数据类型,如果对于对象呢?
如果参数是对象,那么传递的是对象的引用的副本(复制体),这也就是为什么在方法体内对对象进行修改,会真正的改变对象。因为方法体外的引用和方法体内的引用指向的是堆内存中的同一个对象,传递的是对象的引用
如果这里还不能理解值传递,建议先理解一下这一个概念再继续往下看
真的开始分析了
为了你看着方便,栗子代码再来一份:(我真的不是为了凑字数)
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
int t = 0;
try {
return t;
} finally {
++t;
}
}
当代码执行到 return t; 时,并不是直接将t返回了出去,而是将 t 保留了起来(因为还有一个 finally 语句块没有执行!)
并且这个保留,就是值传递性质的一个保留,也就是保留的是t 的一个副本(复制体),我这里先叫他 tt 吧(不是套套!!)
接下来执行 finally 语句块,
finally 中将 t 做了自增运算,t 的确变成了1,
但是这并没有影响 t 的复制体 tt 的值!保留起来的 tt 值还是0!
这个时候执行完了 finally,
正式将保留起来的 tt 返回出去,于是,整个函数的返回结果就是0
这个 t 的副本(复制体)保留的地方是哪儿呢?我查了半天,
有个应该靠谱的说法,保留在函数栈中,但具体保留的区域叫什么,
我也不知道,还请知情大佬指教一下!
上图或许直观一点?
叫我一声灵魂画师我可敢答应!
那么如果,这个 t 是一个对象呢?按照前面说的值传递的问题,如果 t 是一个对象,在 finally 中对 t 进行修改,那么最终返回出去的 t 所显示出来的数据,应该是经过修改的。
public class Test {
public static void main(String[] args) {
Person result = test();
System.out.println(result.age);
}
}
public static Person test() {
Person t = new Person();
t.age = 0;
try {
return t;
} finally {
t.age++;
}
class Person {
int age;
}
这段代码输出的是1,因为 Person 是一个类,t 是一个对象的引用,对象实例保存在堆内存中,t 的副本 tt 也是一个对象的引用,t 和 tt 都指向堆内存中的对象实例,那么不论修改谁,实际上对象实例都被修改了!
看完我这一通胡说八道,你应该了解了整个执行流程咯?
那么继续开一个引申
又一个小栗子
如果在finally中也有一个 return ,会发生什么?
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
int t = 0;
try {
return t;
} finally {
++t;
return t;
}
}
最终输出的结果是1
就是说,如果 try 中有 return 而 finally 中也有 return ,那么后者将会让前者失效!
理解
=> try 中将 t 保留了一份副本用于返回出去,到了 finally 中,又有一个 return 语句,这时候又要创建一个用于返回的副本,那这个时候就有两个副本了,到底返回谁呢?取后者!
总结
这一个面试题,看似简单,却暗藏杀机啊!
可是说了这么多,结果就是 finally 在 return 之后执行吗?
非也,你没看见 return 没有真正的执行完就开始执行 finally 吗?并且是先执行完了 finally,才执行完 return,这也就很好理解 java 规范中的 finally 在 return 之前执行了。
不过,按如上情况,这句话应该变成这样: finally 比 return 先执行完毕。是不是就更容易理解了呢?
也就是说,return 先被执行了,执行 return 的时候发现有 finally,于是不能那么快执行完毕 return,先去执行 finally,等 finally 执行完毕之后,return 才能执行完毕。
全文下来,真是用我的三寸不烂之舌经过滔滔不绝的输出连绵不绝的蛊惑打开了你的新世界大门啊。
(本文转自”Java和Android架构”微信公众号)