前文:
首先,贴一张图和两张表,来展示一下常见的一些异常情况:
常见的error类:
异常类名 | 作用 |
---|---|
LinkageError | 动态链接失败 |
VirtualMachineError | 虚拟机错误 |
AWTError | AWT错误 |
常见运行时异常类:
异常类名 | 作用 |
---|---|
ArithmeticException | 数学运算异常,比如除数为零的异常 |
MissingResourceException | 丢失资源 |
ClassNotFoundException | 指定类或接口不存在的异常(找不到类) |
NullpointerException | 空指针异常 |
IndexOutOfBoundsException | 下标越界异常,比如集合、数组等 |
ArrayIndexOutOfBoundsException | 访问数组元素的下标越界异常 |
StringIndexOutOfBoundsException | 字符串下标越界异常 |
常用的非运行时异常:
异常类名 | 作用 |
---|---|
Ioexception | 输入输出异常 |
FileNotFoundException | 找不到指定文件的异常 |
正文:
遇到异常是每一位程序员一定会经历并且经常在经历的事情,而java在设计之初便已经做了相关的考虑,提出了异常处理框架的这个方案,也就是大概如上面这张图所展示的一些东西。从上图可以看出来的,所有的异常类型都可以用内置类Throwable表示;不同类型的异常对应Throwable不同的子类。
可以看出,Throwable有两大子类:Error和Exception,分别表示错误和异常;其中,Error表示的是不希望被程序捕获或者是程序无法处理的错误,通常是灾难性的错误,基本上是由java虚拟机产生,大多数操作和操作者所执行的操作无关,一般在这种情况下,jvm都会终止线程;而Exception,它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常;这种的话应该尽可能去避免,或者是在程序中去处理;而Exception下分为运行时异常和非运行时异常,其中,非运行异常也叫检查异常,这种必须进行处理,否则程序无法编译通过;而运行时异常则属于不要求强制处理的异常,也叫做不受检查异常,这种的话存在不可查性,所以java规定在运行时如果出现这种异常,系统会自动抛出;但是一般还是需要在程序去规范好,做好约束和限制,多进行测试,尽可能去避免。
那么怎么去进行异常处理呢?上面讲到了java从设计之初就考虑过这方面的问题,那么具体是怎么去做的呢?
其实,对于异常,java有着几种不同的处理方法。我们在日常开发中会遇到一些其他的一些普通问题,这些可能在当前环境下能去获得足够的信息,然后去处理这个问题,但是异常不一样,出现了异常,当前环境没办法获取到足够信息来解决,已经没法再继续下去了,这个时候你要做的就是把异常抛出,跳到另一个环境去获取信息去处理。抛出异常之后,首先会在堆上去new一个异常对象,然后线程终止,当前环境下的程序不再执行,跳出当前环境并且返回在堆上所new的异常对象的引用。然后异常处理机制开始进行处理,寻找一个合适的地方也就是异常处理器或者异常异常处理程序继续去执行,去将程序的错误状态进行修复,让程序继续运行或者换一种方式运行。而这一部分实际上就是异常的抛出-捕获-处理的过程;异常捕获的过程理解上其实也不难,由于异常处理器实际上是存留在栈中的一堆方法的集合,在异常发生时,系统会依次回查调用栈中的方法,一直到找到合适的异常处理器(也就是这个处理器能处理的异常类型和抛出来的异常类型相符)然后调用其方法进行执行处理,如果找不到合适的异常处理器,则系统和程序终止。
java规定:对于所有的检查异常,方法必须对其进行捕获,或者声明将其抛出方法之外;也就是要么在方法内去捕获然后处理,要么把它抛出,让上级环境去处理。java异常处理机制设计五个关键字,分别为:try、catch、throw、throws、finally
try-catch
先贴一段示例:
@Test
public void exceptionTest(){
try {
String a = null;
int b = Integer.parseInt(a);
}catch (NumberFormatException e){
System.out.println("这里输出异常类型"+e);
}
catch (Exception e){
System.out.println("这里输出异常类型"+e);
}
}
运行结果:
这里输出异常类型java.lang.NumberFormatException: null
上面代码中,try块(try后大括号内的内容)里面带着一段可能产生异常的代码,而这就是所谓的监控区域,而catch则放着处理不同异常类型的代码;当在运行过程中发生异常的时候,系统会创建异常对象,然后抛出监控区域之外,然后寻找最近的匹配的catch子句进行处理,只要有一个catch子句匹配上了,则执行完这个catch块内的异常处理代码之后,不再去匹配其他catch块;而匹配的原则是抛出的异常对象属于catch子句的异常类,或者属于该异常类的子类,就算匹配成功。
上面讲到,这种属于运行时异常,实际上允许不进行抛出,系统会自动去抛出。
@Test
public void exceptionTest1(){
String a = null;
int b = Integer.parseInt(a);
}
执行结果:
java.lang.NumberFormatException: null
at java.lang.Integer.parseInt(Integer.java:542)
at java.lang.Integer.parseInt(Integer.java:615)
at com.example.demo.ExceptionTest.exceptionTest1(ExceptionTest.java:28)
可以看到的另一点是,我在上面用了两个catch块,并且NumberFormatException在前,原因在于上面所讲的匹配原则,由于匹配的原则是抛出的异常对象属于catch子句的异常类,或者属于该异常类的子类,就算匹配成功,那么为了捕获更清晰更底层的异常信息,那么要尽量将捕获相对高层的异常类的catch子句放在后面,否则,捕获底层异常类的catch很可能会被屏蔽。不过目前很多工具都会帮助进行检查,如果你把高层的放在前面,往往会引起编译异常。
当try块中可能引起多个异常的时候,在第一个catch子句执行完以后,其他子句被旁路
@Test
public void exceptionTest1(){
String a = null;
int c = 1;
try {
System.out.println("c/1的值是:" + c / 1);
try {
int b = Integer.parseInt(a);
} catch (ArithmeticException e) {
System.out.println("这里输出异常类型" + e);
}
}catch (NumberFormatException e) {
System.out.println("这里输出异常类型" + e);
} catch (Exception e) {
System.out.println("这里输出异常类型" + e);
}
}
结果:
c/1的值是:1
这里输出异常类型java.lang.NumberFormatException: null
上面这段代码可以看到,try块里面嵌套着一个try块,而嵌套的try块会产生一个NumberFormatException异常,但是本身又不能对这个异常进行处理,在这种情况下,它会把异常往外抛,外面的catch如果能进行处理,那么就会进行处理;如果不能,则程序中断。
在很多时候,我们会经历很多这样的情况,因为我们会经常进行方法调用,而调用的方法内部可能就有异常捕获处理,这就形成了隐蔽式的异常嵌套。
throw
上面所讲到的都是对运行时的异常进行处理,同样的,java存在throw关键字会一些比较明确的异常做直接抛出,当程序在try块内执行throw子句之后,程序后面的内容不再执行,转向寻找最近的匹配的catch块进行处理,如果没有匹配的catch,则一样转向调用默认的异常处理程序的中断程序并打印堆栈轨迹。 需要注意的是,throw的使用如下:
throw new ThrowableInstance
这里还有一点需要注意的,就是需要使用new来构造一个异常实例,而所有的Java内置的运行时异常有两个构造方法:一个没有参数,一个带有一个字符串参数。 当用第二种形式时,参数指定描述异常的字符串。如果对象用作print()或者println()的参数时,该字符串被显示。这同样可以通过调用getMessage()来实现,getMessage()是由Throwable定义的。
throws
上面讲到,如果当前方法可以对产生的异常不处理,但是必须将其声明并抛出方法之外,这样才能保证自己能编译通过,java提供了throws关键字来完成这样的工作,其通常声明形式如下:
public void method throws Exception{
// method body
}
这里需要注意的一点是,你需要把所有可能产生的异常都抛出,中间用逗号隔开即可,而如果产生的是error或者RuntimeException,那么可以不抛出,不影响编译,只是运行时系统也会自动抛出;而如果方法使用throws将异常进行抛出,那么该方法的调用者也必须符合Java对于异常处理机制的原则,即要么进行捕获处理,要么继续声明并抛出方法之外,让更上一级的环境去进行处理,并且调用方法声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
finally
由于异常处理机制往往会让你的某些后续代码旁路,而你有时候并不希望出现这样的情况,这个时候,你就可以使用finally进行处理。
使用finally创建的代码块无论你的方法到底有没有抛出异常捕获异常处理异常,它都会在方法返回之前执行,往往在进行资源分配调度和释放以及一些优化会很有用。
java中规定:finally子句是可选项,可以有也可以无,但是每个try语句至少需要一个catch或者finally子句。
异常链
异常链顾名思义就是将发生的异常一个传一个一层接着一层串起来,把底层的异常信息传给上层,不断去逐层抛出。 Java API文档中给出了一个简单的模型:
try {
lowLevelOp();
} catch (LowLevelException le) {
throw (HighLevelException) new HighLevelException().initCause(le);
}
当程序捕获到了一个底层异常,在处理部分选择了继续抛出一个更高级别的新异常给此方法的调用者。这样就可以进行逐层传递。最终,位于高层的异常递归调用getCause()方法,就可以遍历各层的异常原因。 这就是Java异常链的原理。实际上,异常链的实际应用很少,因为这个最终 上层拿到这些异常还是很难去进行处理,反而在这样一个逐层上抛过程中会消耗大量资源,因为你要保存一个完整的异常链信息。要知道,不发生异常还好,当你发生之后,catch的执行可是本来就消耗着大量的资源,何况你还要不断抛出让保存整条异常链的完整信息。其实这也告诉我们,在日常开发中,要尽量少写不应该存在的异常程序,优雅的去进行异常的处理,并且,并不是你的每一块代码块都去try-catch就很好,确实,很多时候为了防止应用崩溃我们可能需要去使用这样的手段,但是同样的,我们也要注意资源的消耗和利用,而且,程序中疯狂的使用try-catch对于代码阅读以及维护很不友好!!!(我的个人看法个人理解,欢迎反驳)
最后加一点很很很基础的东西,很多人可能最开始都会不知道一个包含异常处理块的方法最终的方法返回应该放在哪里,其实很简单,你要么在程序最后面去return,要么在try块和catch块中分别return,或者在finally中去return,这样就可以了。