要想弄清楚异常的一些知识点,比如try/finally的return,异常的丢失等等,光记住“形式”是不够的,不从字节码异常表的层面分析是很难真正的将一些问题弄明白。
(1)判断程序的返回值:
private int hasException() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
结果:
(1)未发生异常,返回1;
(2)发生Exception异常返回2;
(3)发生其他异常,没有返回值;
以正常返回为例分析:
Java中返回值实际上是由一个returnValue变量保存的,编译器会将finally中的逻辑查到return之前,通过查看字节码可以知道,变量x等于1时,赋值给了returnValue变量,之后才赋值为3的,因此最后返回3。
0: iconst_1 //常量1压入操作数栈
1: istore_1 //x=1,保存到局部变量表slot 1
2: iload_1 //局部变量表slot 1也就是x压入操作数栈
3: istore_2 //栈顶值“1”保存到slot 2,returnValue = 1
4: iconst_3 //常量3压入操作数栈
5: istore_1 //栈顶值“3”保存到局部变量表slot 1, x=3
6: iload_2 //returnValue压入操作数栈
7: ireturn //返回栈顶元素,returnValue的值
(2) finally中的return:
private int finallyReturn() {
int x;
try {
x = 1;
return x;
} catch(Exception e) {
x = 2;
return x;
} finally {
x = 3;
return 3;
}
}
结果:
正常返回或者Exception异常或者其他异常都返回3;
分析:
正如前叙,编译器会将finally中的逻辑插入各个分支之中。因此try和catch中的return都不会执行到(被编译器忽略了)。查看字节码:
6: iconst_3 //常量3压入操作数栈
7: ireturn //返回栈顶元素3
PS:
能看懂字节码,很多问题轻松弄明白。
上述字节码使用javap -verbose -private xxxx输出查看。
后面finally子句小节中有更系统的解释。
基类:Throwable;
子类划分:Error(内部错误/资源耗尽错误),Exception(又又子类RuntimeException);
是否为受检查异常:
checked异常:Exception,因为为一些不可预测的情况导致的异常,checked的目的是为了让我们能够给出相应的恢复手段;
unchecked异常:Error和RunntimeException,后者一般是“程序员自己的问题导致的”;
throws关键字;
class文件中可以通过方法表的Exceptions属性查看(这是javap输出的,实际上Exceptions中保存的是异常类全限定名在常量池中的索引):
Exceptions:
throws java.io.IOException, java.sql.SQLException
子类重写父类方法,如果父类方法声明了异常,子类方法可以:
(1)不声明异常;
(2)声明父类方法中异常或其子类异常;
private static class SuperClass {
protected void f() throws IOException {}
}
private static class SubClass1 extends SuperClass {
@Override
protected void f() throws ZipException {}
}
private static class SubClass2 extends SuperClass {
@Override
protected void f() {}
}
在catch语句中重新包装异常,通过设置initCause的方式保证不丢失原始异常:
private void catchAndThrowNew() throws Throwable {
try {
throw new SQLException();
} catch (SQLException e) {
Throwable se = new ServletException();
se.initCause(e);
throw se;
}
}
正如开头的例子:
(1)当try子句中有return语句,编译器将产生一个中间变量“returnValue”,保存try中return语句中变量在try语句中的最后赋值;
(2)当finally子句中有return语句,将“覆盖”try中的return;
(3)当finally子句中没有return语句,try中的return因为有returnValue值,不会受finally的影响;
(1)如果不同层次的finally子句中包含return,内层的return语句将被忽略,最外层的return才有效;
(2)如果一个内层的finally子句中包含return,外层还有finally子句(没有return),编译器同样会生成一个“returnValue”中间变量,保证内层的return逻辑,可见Java设计的思想在于保证“可见”逻辑的正确性;
(3)执行顺序是先内后外;
private int nestedTryCatch() {
int x;
try {
try {
x = 2;
} finally {
x = 3;
return x;
}
} finally {
x = 4;
return x;
}
}
JSE 7中提供了“try-with-resources”的简化形式:
private void tryWithResources() throws IOException {
try(InputStream in = new ByteArrayInputStream(new byte[10])) {
try (Reader reader = new BufferedReader(new InputStreamReader(in))) {
}
}
}
注意:带资源的try语句也可以有自己的catch和finally,但是要在资源关闭之后执行。
情形1:finally中的新异常:
try {
throw new IllegalArgumentException();
} finally {
throw new IllegalStateException();
}
情形2:finally中的return:
try {
throw new IllegalArgumentException();
} finally {
return;
}
情形3:finally中没有新异常和return:
try {
throw new IllegalArgumentException();
} finally {
int x = 3;
}
首先,athrow指令是将栈顶异常引用抛出:
(1)情况1的字节码:
Code:
stack=2, locals=2, args_size=1
0: new #21 // class java/lang/IllegalArgumentException
3: dup
4: invokespecial #22 // Method java/lang/IllegalArgumentException."<init>":()V
7: athrow
8: astore_1
9: new #23 // class java/lang/IllegalStateException
12: dup
13: invokespecial #24 // Method java/lang/IllegalStateException."<init>":()V
16: athrow
Exception table:
from to target type
0 9 8 any
分析:
从异常表中可以看到,0到9行中如果有任何的异常出现,转到8行进行处理,第8行之后就是finally之后的逻辑重新抛出了新的异常;
(2)情形3字节码分析:
Code:
stack=2, locals=3, args_size=1
0: new #21 // class java/lang/IllegalArgumentException
3: dup
4: invokespecial #22 // Method java/lang/IllegalArgumentException."<init>":()V
7: athrow
8: astore_1
9: iconst_3
10: istore_2
11: aload_1
12: athrow
Exception table:
from to target type
0 9 8 any
分析:异常表的逻辑和情形1是相同的,但是finally的逻辑对应的字节码将局部变量表中的异常引用取出执行了athrow;
(1) Throwable的方法:printStackTrace和getStackTrace;
(2)Thread.getAllStackTraces();
(1)不能用异常处理代替逻辑判断,因为异常处理的性能开销较大;
(2)不要过分细化异常及处理;
(3)不要压制异常,异常要么声明,要么正确在该方法内处理;
(4)设计具有意义的自定义异常体系;
PS:其他一些好的做法向《Efftive Java》中说的很详细。
就我的经验来看,还是使用开源的框架中的断言(比如Spring Framework中的断言)和日志(比如Log4j)比较合适。