先看一道题,输出啥?
package Test;
public class TestException {
public TestException() {
}
boolean testEx() throws Exception {
boolean ret = true;
try {
ret = testEx1();
} catch (Exception e) {
System.out.println("testEx, catch exception");
ret = false;
throw e;
} finally {
System.out.println("testEx, finally; return value=" + ret);
return ret;
}
}
boolean testEx1() throws Exception {
boolean ret = true;
try {
ret = testEx2();
if (!ret) {
return false;
}
System.out.println("testEx1, at the end of try");
return ret;
} catch (Exception e) {
System.out.println("testEx1, catch exception");
ret = false;
throw e;
} finally {
System.out.println("testEx1, finally; return value=" + ret);
return ret;
}
}
boolean testEx2() throws Exception {
boolean ret = true;
try {
int b = 12;
int c;
for (int i = 2; i >= -2; i--) {
c = b / i;
System.out.println("i=" + i);
}
return true;
} catch (Exception e) {
System.out.println("testEx2, catch exception");
ret = false;
throw e;
} finally {
System.out.println("testEx2, finally; return value=" + ret);
return ret;
}
}
public static void main(String[] args) {
TestException testException1 = new TestException();
try {
testException1.testEx();
} catch (Exception e) {
e.printStackTrace();
}
}
}
如果你一眼就能看出答案,那么这篇文章你就不用浪费时间看啦。输出结果为:
i=2
i=1
testEx2, catch exception
testEx2, finally; return value=false
testEx1, finally; return value=false
testEx, finally; return value=false
为什么要使用异常?
在没有异常机制的时候比如C语言,我们是这样处理的:通过函数的返回值来判断是否发生了异常(这个返回值通常是已经约定好了的),调用该函数的程序负责检查并且分析返回值。虽然可以解决异常问题,但是这样做存在几个缺陷:
- 容易混淆。如果约定返回值为-11111时表示出现异常,那么当程序最后的计算结果真的为-1111呢?
- 代码可读性差。将异常处理代码和程序代码混淆在一起将会降低代码的可读性。
- 由调用函数来分析异常,这要求程序员对库函数有很深的了解
在java中提供了异常处理机制能够有效将正常业务代码和异常代码分离开来。
使用异常机制它能够降低错误处理代码的复杂度,如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它,而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误,并且,只需在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节约代码,而且把“概述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的阅读、编写和调试工作更加井井有条。
异常分类
在java中,所有的异常都有一个共同的祖先,Throwable,它有两个重要的子类,Exception(异常)和Error(错误)。Error是指程序无法处理的错误,通常是指jvm运行时出现的问题,比如当jvm不再具有继续执行操作所需的内存资源时,将抛出OutOfMemory,当error发生时,jvm一般选择终止线程。
Exception是指由于程序逻辑本身的问题,程序本身可以处理的异常。RuntimeException是其一个重要子类。
java异常通常又分为可查的异常和不可查的异常,可查的异常在一定程度上是可以预计的,在编译阶段就需要处理,否则编译通不过。在Exception中,除了RuntimeException及其子类外,其他Excetpion及其子类都属于可查异常。不可检查异常包括运行时异常(RuntimeException)和错误(Error)。
try catch finally 字节码分析
static int inc(){
int x;
try {
x = 1;
return x;
} catch (Exception e){
x = 2;
return x;
} finally {
x = 3;
}
}
static int inc();
descriptor: ()I
flags: ACC_STATIC
Code:
stack=1, locals=4, args_size=0
0: iconst_1 //try 块中的 x = 1;
1: istore_0 //保存栈顶元素到局部变量表中索引为 0 的 slot 中
2: iload_0 //加载局部变量表中索引为 0 的值到栈中
3: istore_1 //保存栈顶元素到局部变量表中索引为 1 的 slot 中
4: iconst_3 //finally 块中的 x = 3;
5: istore_0 //保存栈顶元素到局部变量表中索引为 0 的 slot 中,x 的值存在这里。
6: iload_1 //加载局部变量表中索引为 1 的值到栈中
7: ireturn //返回栈顶元素,即 x = 1;正常情况下函数运行到这里就结束了,如果出现异常根据异常表跳转到指定的位置
8: astore_1 //给 catch 块中定义的 Exception e 赋值,存储在 slot1 中。
9: iconst_2 //catch 块中的 x = 2;
10: istore_0
11: iload_0
12: istore_2
13: iconst_3 //finally 块中的 x = 3;
14: istore_0
15: iload_2
16: ireturn //此时返回的是 slot2 中的值,即 x = 2
17: astore_3 //如果出现不属于 java.lang.Exception 及其子类的异常,才会根据异常表中的规则跳转到这里。
18: iconst_3 //finally 块中的 x = 3;
19: istore_0
20: aload_3 //将异常加载到栈顶,
21: athrow //抛出栈顶的异常
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
可以看到,Java 的异常处理是通过异常表的方式来决定代码执行的路径。而finally的实现是通过在每个路径的最后加入finally块中的字节码实现的。
try catch 会不会影响性能
try {
for(int i = 0; i < max; i++) {
String myString = ...;
float myNum = Float.parseFloat(myString);
myFloats[i] = myNum;
}
} catch (NumberFormatException ex) {
return null;
}
for(int i = 0; i < max; i++) {
String myString = ...;
try {
float myNum = Float.parseFloat(myString);
} catch (NumberFormatException ex) {
return null;
}
myFloats[i] = myNum;
}
上述性能区别在哪?想当然就觉得try catch重复执行了这么多次肯定比只执行了一次跑得肯定慢,空间消耗肯定更大,实际情况是不是就是这样呢?
从上小节字节码可知,每个类会跟随一张异常表(exception table),每一个try catch都会在这个表里添加行记录,每一个记录都有4个信息(try catch的开始地址,结束地址,异常的处理起始位,异常类名称)。
当代码在运行时抛出了异常时,首先拿着抛出位置到异常表中查找是否可以被catch(例如看位置是不是处于任何一栏中的开始和结束位置之间),如果可以则跑到异常处理的起始位置开始处理,如果没有找到则原地return,并且copy异常的引用给父调用方,接着看父调用的异常表。。。以此类推。
也就是说,异常如果没发生,也就不会去查表,也就是说你写不写try catch 也就是有没有这个异常表的问题,如果没有发生异常,写try catch对性能是木有消耗的,所以不会让程序跑得更慢。try 的范围大小其实就是异常表中两个值(开始地址和结束地址)的差异而已,也是不会影响性能的。
异常处理机制
当一个方法出现错误引发异常时,运行时系统创建异常对象,该对象包括了异常类型和堆栈信息等,接着运行时系统负责寻找合适的异常处理器(exception handler),潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合,当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,就是合适的异常处理器,运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适 的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。
异常的使用建议
OutputStreamWriter out = null;
java.sql.Connection conn = null;
try { // ---------1
Statement stat = conn.createStatement();
ResultSet rs = stat.executeQuery("select *from user");
while (rs.next()){
out.println("name:" + rs.getString("name") + "sex:"
+ rs.getString("sex"));
}
conn.close(); //------2
out.close();
}
catch (Exception ex){ //------3
ex.printStackTrace(); //------4
}
- 对于这个try…catch块,他真正目的是捕获SQL的异常,但是这个try块包含了太多的信息了。这是我们为了偷懒而养成的代码坏习惯。有些人喜欢将一大块的代码全部包含在一个try块里面,因为这样省事,反正有异常抛出它就会捕获,而不愿意花时间来分析这个大代码块有哪几块会产生异常,产生什么类型的异常,反正就是一篓子全部搞定。这就想我们出去旅游将所有的东西全部装进一个箱子里面,而不是分类来装,虽然装进去容易,但找出来难!!!所有对于一个异常块,我们应该仔细分清楚每块的抛出异常,因为一个大代码块有太多的地方会出现异常了。所以应该尽可能减小try块范围!!!
- 在这里你发现了什么?异常改变了运行流程!!如果该程序发生了异常那么conn.close(); out.close();是不可能执行得到的,这样势必会导致资源不能释放掉。所以如果程序用到了文件、Socket、JDBC连接之类的资源,即使遇到了异常,我们也要确保能够正确释放占用的资源。这里finally就有用武之地了:不管是否出现了异常,finally总会执行的,所以应当充分利用finally释放资源。
- 使用这样代码的人都有这样一个心理,一个catch解决所有异常,这样是可以,但是不推荐!为什么!首先我们需要明白catch块所表示是它预期会出现何种异常,并且需要做何种处理。catch语句应当尽量指定具体的异常类型,而不应该指定涵盖范围太广的Exception类。
- 捕获异常不做处理,就是我们所谓的丢弃异常。我们都知道异常意味着程序出现了不可预期的问题,程序它希望我们能够做出处理来拯救它,但是你呢?一句ex.printStackTrace()搞定,这是多么的不负责任对程序的异常情况不理不顾。既然捕获了异常,就要对它进行适当的处理,比如:
- 对所发生的的异常进行一番处理,如修正错误、提醒.
- 重新抛出异常。既然你认为你没有能力处理该异常,那么你就尽情向上抛吧!!!
- 封装异常。对异常信息进行分类,然后进行封装处理。
- 打印出明确的异常信息。在出现异常后,我们最好能够提供一些文字信息,例如当前正在执行的类、方法和其他状态信息,包括以一种更适合阅读的方式整理和组织printStackTrace提供的信息。
网友评论