在使用try、catch、finally语句时,出现了一些疑问。看代码:
package test;
public class Test {
public String test() {
String t = "";
try {
t = "try";
return t;
} finally {
t = "finally";
}
}
}
首先程序执行try语句块,把变量t赋值为try,接下来执行finally语句块,把变量t赋值为finally,然后return t,那么程序结果应该显示finally,但是实际结果为try。
使用反编译命令javap -v -p Test看看编译出来的字节码的信息:
运行环境是Windows7(64位) + Java JDK 1.7.0
(提示:很早之前(JDK 1.4.2 之前)的 Sun Javac 已经不再为 finally 语句生成 jsr 和 ret 指令了,
而是改为在每个分支之后冗余代码的形式来实现 finally 语句)
尝试分析一下上面的字节码,如果有什么错误的地方,请指正,感谢!
在此之前,可以先了解一下栈帧、局部变量表、操作数栈的概念,
本地变量表:
可以看到有两个本地变量,一个是this一个是t。
(如果是静态方法,方法内声明的变量在slot中的存放从0开始,以变量声明的顺序存放,如果是普通方法,则slot中0的位置存放this变量,其他在方法中声明的变量从1开始存放,依旧以变量声明的顺序存放。)
异常表:
从异常表可以看到如果从3 ~ 8出现异常,从13开始运行。
常量池:
Code:
这一行是所有的方法都会有的,其中Stack我理解为方法生命周期内栈的最大深度。
Locals是本地变量的slot个数,但是并不代表是stack宽度一致,本地变量是在这个方法生命周期内,局部变量最多的时候,需要多大的宽度来存放数据(double、long会占用两个slot)。
Args_size代表的是入参的个数,不再是slot的个数,也就是传入一个long,也只会记录1。该方法显示的参数指的是this,如果是无参的静态方法Args_size会是0。
0: ldc #16 // String; 从常量池#16的位置取出“”这个字符串(对象引用)压栈,此时栈深度为1
2: astore_1 // 弹出栈顶的String对象的引用并将其存入slot1处的局部变量t中,此时t=“”,此时栈深度为0
3: ldc #18 // String try; 从常量池#18的位置取出“try”这个字符串(对象引用)压栈,此时栈深度为1
5: astore_1 // 弹出栈顶的String对象的引用并将其存入slot1处的局部变量t中,此时t=“try”,此时栈深度为0
6: aload_1 // 将slot1处的局部变量t压入栈顶,此时栈深度为1
7: astore_3 // 弹出栈顶的String对象的引用并将其存入第四个局部变量中,该变量的值为“try”,此时栈深度为0
8: ldc #20 // String finally; 从常量池#20的位置取出“finally”这个字符串(对象引用)压栈,此时栈深度为1
10: astore_1 // 弹出栈顶的String对象的引用并将其存入slot1处的局部变量t中,此时t=“finally”,此时栈深度为0
11: aload_3 // 将第四个局部变量压入栈顶,此时栈深度为1
12: areturn // 正常返回(没有异常发生), 返回栈顶值,也就是第四个局部变量,它指向“try”字符串对象,此时栈深度为0
13: astore_2 // (如果3 ~ 8中出现异常,则跳到此指令开始执行)弹出栈顶的异常对象引用并将其存入第三个局部变量中
14: ldc #20 // String finally, 从常量池#20的位置取出“finally”这个字符串(对象引用)压栈,此时栈深度为1
16: astore_1 // 弹出栈顶的String对象的引用并将其存入slot1处的局部变量t中,此时t=“finally”,此时栈深度为0
17: aload_2 // 将存储在第三个局部变量中异常引用压入栈顶,此时栈深度为1
18: athrow // 将栈顶的异常抛出,此时栈深度为0
从字节码中可以看到在try块中给局部变量t指向了字符串对象“try”后执行return t时,JVM先把字符串对象“try”的引用缓存到一个局部变量中,该局部变量存储在本地变量表的Slot 3位置。然后执行finally块,在finally块中,局部变量t指向了字符串对象“finally”,当执行完finally块后,把本地变量表的Slot 3位置存储的局部变量返回,此时,该局部变量指向的是字符串对象“try”,所以返回值是“try”而不是“finally”。
所以可以得出结论是:
如果在执行finally块前出现return语句,会把在值先缓存起来,等执行完finally块后,再返回缓存起来的值。
验证一下:
package test;
import java.util.ArrayList;
import java.util.List;
public class Test {
public static List<String> test() {
List<String> list = new ArrayList<>();
try {
list.add("a");
return list;
} finally {
list = new ArrayList<>();
list.add("b");
}
}
public static List<String> test1() {
List<String> list = new ArrayList<>();
try {
list.add("a");
return list;
} finally {
list.add("b");
}
}
public static Car test2() {
Car car = new Car();
try {
car.setSize(1);
return car;
} finally {
car = new Car();
car.setSize(2);
}
}
public static Car test3() {
Car car = new Car();
try {
car.setSize(1);
return car;
} finally {
car.setSize(2);
}
}
public static int test4() {
int i = 0;
try {
i = 1;
return i;
} finally {
i = 2;
}
}
public static int test5() {
int i = 0;
try {
throw new RuntimeException();
} catch (Exception e) {
i = 1;
return i;
} finally {
i = 2;
}
}
public static Car test6() {
Car car = new Car();
try {
throw new RuntimeException();
} catch (Exception e) {
car.setSize(1);
return car;
} finally {
car.setSize(2);
}
}
public static void main(String[] args) {
System.out.println("-------在try块中返回-------");
System.out.println(test());
System.out.println(test1());
System.out.println(test2());
System.out.println(test3());
System.out.println(test4());
System.out.println("-------在catch块中返回-------");
System.out.println(test5());
System.out.println(test6());
}
}
class Car {
private int size;
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
@Override
public String toString() {
return "car [size=" + size + "]";
}
}
控制台:
-------在try块中返回-------
[a]
[a, b]
car [size=1]
car [size=2]
1
-------在catch块中返回-------
1
car [size=2]
如果是返回基本类型的值,那么在缓存时也是缓存值本身,所以后面在finally块中重新赋值时,方法返回的值不会受finally块中重新赋值的影响;
如果返回的是引用类型的值,那么在缓存时,缓存的是引用类型对象的引用,所以虽然后面在finally块中重新赋值时(重新指向另一个对象),方法返回的值不会受到影响,但是如果是修改对象的属性,那么会影响到返回的值。