什么是异常
异常是指不期而至的各种状况,从而阻止当前方法或作用域继续执行。如:数组下标越界、文件找不到等等。异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程。
Java通过Throwable类的众多子类描述各种不同的异常:
Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,Java虚拟机(JVM)一般会选择线程终止。
Exception类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
Exception类又分为运行时异常(Runtime Exception)和受检查的异常(Checked Exception,也就是非运行时异常 ):
- 运行时异常:这类异常会自动被虚拟机抛出,所以不必在异常说明中把它们列出来。这类异常也被称为不受检查异常,程序中可以选择捕获处理,也可以不处理,都可以成功编译。
- 受检查的异常:是RuntimeException及其子类以外的异常,此类异常必须在程序中捕获处理,或者抛给它的调用者,让上一层去处理该异常。否则程序不能通过编译。
对于受检查的异常必须捕捉、或者声明抛出。允许忽略RuntimeException和Error。
异常处理机制
当抛出异常后,Java将使用new在堆上创建异常对象,然后当前的执行路径被终止,并且从当前的环境中弹出对异常对象的引用。此时,异常处理机制接管程序,寻找并执行异常处理程序,异常处理程序就是捕获处理异常的地方,它的任务就是将程序从错误状态恢复,以使程序要么换一种方式运行,要么继续运行下去。
相关概念跟语句
抛出异常
当一个方法出现错误引发异常时,方法创建异常对象并交付运行时系统,异常对象中包含了异常类型和异常出现时的程序状态等异常信息。运行时系统负责寻找处置异常的代码并执行。
捕获异常
在方法抛出异常之后,运行时系统将转为寻找合适的异常处理器(exception handler)。潜在的异常处理器是异常发生时依次存留在调用栈中的方法的集合。当异常处理器所能处理的异常类型与方法抛出的异常类型相符时,即为合适的异常处理器。运行时系统从发生异常的方法开始,依次回查调用栈中的方法,直至找到含有合适异常处理器的方法并执行。当运行时系统遍历调用栈而未找到合适的异常处理器,则运行时系统终止。同时,意味着Java程序的终止。
异常处理程序
捕获处理异常通过try、catch语句完成 ,其语法为:
try {
// 可能会发生异常的程序代码
} catch (Type1 id1){
// 捕获并处置try抛出的异常类型Type1
}
catch (Type2 id2){
//捕获并处置try抛出的异常类型Type2
}
try块称为监控区域,其中包含着可能发生异常的代码。Java方法在运行过程中出现异常,则创建异常对象。将异常抛出监控区域之外,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序(catch块),然后进入catch子句执行,此时认为异常得到了处理。一旦catch子句结束,则处理程序的查找过程结束,这意味着其他的catch子句不再有和被捕获异常类型匹配的机会。
try-catch语句后面还可以跟着finally子句,finally子句无论是否出现异常都会被执行,其语法为:
try {
// 可能会发生异常的程序代码
} catch (Type1 id1) {
// 捕获并处理try抛出的异常类型Type1
} catch (Type2 id2) {
// 捕获并处理try抛出的异常类型Type2
} finally {
// 无论是否发生异常,都将执行的语句块
}
finally子句也不总是会被执行,在System.exit()(终止JVM)或者当前线程死亡的情况下,finally子句不会被执行。
异常说明
当一个方法出现异常,但没有能力处理这种异常时,便可以将其抛给其调用者去处理,此时需要用到异常说明。
Java鼓励人们把方法可能会抛出的异常告知所有此方法的客户端程序员,这样做使得调用者能确切知道写什么代码可以捕获所有潜在的异常。其相应的语法为:
方法名 throws Exception1,Exception2,..,ExceptionN {
}
异常说明属于方法声明的一部分,紧跟在形式参数列表之后,Exception1,Exception2,…,ExceptionN 为声明要抛出的异常列表。当方法抛出异常列表中的异常时,方法将不对这些类型及其子类类型的异常作处理,而抛向调用该方法的方法,由调用者去处理,如果方法调用者也处理不了,可以继续抛出异常,但最终要有能够处理该异常的调用者,否则程序将被终止。
即使方法不产生异常,也可以使用异常说明,为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。这种用法在定义抽象类和接口时经常使用到,这样其派生类或接口实现就可以抛出这些预先声明的异常。
使用throws声明异常时,要注意:
必须声明方法抛出的任何受检查异常(checked exception)。即如果一个方法出现受检查异常,要么用try-catch语句捕获,要么用throws子句声明将它抛出,否则会导致编译错误。
若覆盖一个方法,则不能声明与覆盖方法不同的异常。声明的任何异常必须是被覆盖方法所声明异常的同类或子类。
void method1() throws IOException{} //合法
//编译错误,必须捕获或声明抛出IOException
void method2(){
method1();
}
//合法,声明抛出IOException
void method3()throws IOException {
method1();
}
//合法,声明抛出Exception,IOException是Exception的子类
void method4()throws Exception {
method1();
}
//合法,捕获IOException
void method5(){
try{
method1();
}catch(IOException e){…}
}
//编译错误,必须捕获或声明抛出Exception
void method6(){
try{
method1();
}catch(IOException e){throw new Exception();}
}
//合法,声明抛出Exception
void method7()throws Exception{
try{
method1();
}catch(IOException e){throw new Exception();}
}
使用throw关键字抛出异常
throw关键字用来抛出一个Throwable类型的异常。该方法会在throw语句后停止执行其正常流程,转而执行catch子句或者finally子句,甚至程序终止。
我们知道,异常是异常类的实例对象,我们可以创建异常类的实例对象通过throw语句抛出。该语句的语法格式为:
throw new Exception;
例如抛出一个IOException类的异常对象:
throw new IOException;
使用throw语句抛出了受检查异常,应该在方法头部声明方法可能抛出的异常类型。该方法的调用者也必须检查处理抛出的异常。
使用throw new Exception
语句抛出异常,在new创建了异常对象之后,此对象的引用将传给throw。throw产生的效果类似与使用return返回,可以简单地把它看成一种不同的返回机制,使用throw还可以从当前的作用域退出,在这两种情况下,将会返回一个异常对象,然后退出作用域或方法。
注意:当一个方法同时出现了多个throw或者return语句时,后续执行的throw或者return语句会覆盖掉前面执行的throw或者return语句。
当finally子句中出现throw或者return语句时,就会出现这种现象,所以finally子句中不应该出现return语句。
例如:
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
异常链
在捕获一个异常后抛出另一个异常,并且把原始异常的信息保存下来,这被称为异常链。
所有Throwable的子类在构造器中都可以接受一个cause(Throwable类型)对象作为参数,这个cause就用来表示原始异常,这样通过把原始异常传递给新的异常,使得即使在当前位置创建并抛出异常,也能通过这个异常链追踪到最初异常的位置。
如果是没有提供带cause参数构造器的异常,可以使用initCause(Throwable cause)方法把其他类型的异常链接起来。
例子:
没有使用异常链:
public class ExceptionChain {
public static void main(String[] args) {
try {
m2();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void m2() {
try{
m1();
}
catch (Exception e) {
throw new RuntimeException();
}
}
public static void m1() {
throw new NullPointerException();
}
}
控制台:
java.lang.RuntimeException
at test.ExceptionChain.m2(ExceptionChain.java:18)
at test.ExceptionChain.main(ExceptionChain.java:7)
使用异常链:
public class ExceptionChain {
public static void main(String[] args) {
try {
m2();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void m2() {
try{
m1();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void m1() {
throw new NullPointerException();
}
}
控制台:
java.lang.RuntimeException: java.lang.NullPointerException
at test.ExceptionChain.m2(ExceptionChain.java:18)
at test.ExceptionChain.main(ExceptionChain.java:7)
Caused by: java.lang.NullPointerException
at test.ExceptionChain.m1(ExceptionChain.java:23)
at test.ExceptionChain.m2(ExceptionChain.java:15)
... 1 more
通过异常链可以将受检查的异常转换成“不受检查的异常”。如果在一个方法中出现了受检查的异常,但是在此方法中不知道该如何处理这个异常,也不想打印一些无用的信息时,可以把受检查的异常屏蔽掉,不用“吞掉”异常,也不必把它放到方法的异常说明里,而且异常链还能保证你不会丢失任何原始异常的信息。
例如:
public class ExceptionChain {
public static void main(String[] args) {
try {
m2();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void m2() {
m1();
}
public static void m1() {
try {
throw new ClassCastException();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
控制台:
java.lang.RuntimeException: java.lang.ClassCastException
at test.ExceptionChain.m1(ExceptionChain.java:23)
at test.ExceptionChain.m2(ExceptionChain.java:16)
at test.ExceptionChain.main(ExceptionChain.java:9)
Caused by: java.lang.ClassCastException
at test.ExceptionChain.m1(ExceptionChain.java:21)
... 2 more
常见的异常
runtimeException子类:
java.lang.ArrayIndexOutOfBoundsException
数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。java.lang.ArithmeticException
算术条件异常。譬如:整数除零等。java.lang.NullPointerException
空指针异常。例如:调用null的实例方法、访问null的属性等等。java.lang.NegativeArraySizeException
数组长度为负异常。java.lang.ArrayStoreException
数组中包含不兼容的值抛出的异常。java.lang.SecurityException
安全性异常。java.lang.IllegalArgumentException
非法参数异常。
受检查异常:
java.lang.ClassNotFoundException
找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。IOException
操作输入流和输出流时可能出现的异常。EOFException
当输入过程中意外到达文件或流的末尾时,抛出此异常。FileNotFoundException
文件未找到异常。ClassCastException
类型转换异常。SQLException
数据库操作异常。NoSuchFieldException
字段未找到异常。NoSuchMethodException
方法未找到异常。NumberFormatException
字符串转换为数字抛出的异常。StringIndexOutOfBoundsException
字符串索引超出范围抛出的异常。IllegalAccessException
安全权限异常。通常是通过反射调用了private方法所导致的。InstantiationException
当应用程序试图使用Class类中的newInstance()方法创建一个类的实例,而指定的类对象无法被实例化时,抛出该异常。
异常使用指南
应该在下列情况下使用异常:
- 在恰当的级别处理异常。(在知道该如何处理的情况下才捕获异常。)
- 解决问题并且重新调用产生异常的方法。
- 进行少许修改,然后绕过异常发生的地方进行执行。
- 用别的数据进行计算,以代替方法预计会返回的值。
- 把当前运行环境下能做完的事情尽量做完,然后把相同的异常重抛到更高层。
- 把当前运行环境下能做完的事情尽量做完,然后把不同的异常抛到更高层。
- 终止程序。
- 进行简化。(如果你的异常模式使问题变得复杂,那用起来会非常痛苦也很烦人。)
- 让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。)