导语: 代码运行时,总会出现错误或者是异常。因此,Java从C++继承了以面向对象方式处理异常的机制,用对象的方式来表示一个(或一类)异常,从而使开发人员写出具有容错性的健壮的代码。
异常概述
Java异常体系结构
首先介绍一下Java异常体系结构,如下图所示:
在Java中,任何异常都是Throwable
类或其子类对象;Throwable有两个子类,分别是:Error
和Exception
;
Error
:这是系统错误类,是程序运行时Java内部的错误,一般是由硬件或操作系统引起的,开发人员一般无法处理,这类异常发生时,只能关闭程序。
Exception
:这是异常类,该类及其子类对象表示的错误一般是由算法考虑不周或编码时疏忽所致,需要开发人员处理。
Java异常分类
Java中的异常被分为两大类:Runtime异常
和非Runtime异常
,其中非Runtime异常也被称作Checked异常
。
Runtime异常
:是Java程序运行时产生的异常,运行时异常不需要开发人员手动去处理,jvm会帮我们处理这类异常。典型的运行时异常有:数组下标越界异常(IndexOutOfBoundsException)、空指针异常(NullPointerException)、对象类型强制转换异常(ClassCastException)以及数组存储异常(ArrayStoreException,即数组存储类型不一致)等。Error
和RuntimeException
都是Runtime异常,编译时对这类异常不做检查。
非Runtime异常
:也称Checked异常
,Checked异常必须由开发人员手动捕获然后处理或者抛出该异常,因为Java认为Cheked异常都是可以修复的异常。例如IOException、SqlException等。在编译时,编译器会对这类异常进行检查,看看有没有对这类异常进行处理,如果没有进行处理,编译会无法通过。
异常处理机制
异常的捕获与处理
Java采用try-catch-finally语句来对异常进行捕获并处理。该语句的语法格式如下:
try{
//可能会产生异常的代码
}
catch(Exception_1 e1){
//处理Exception_1的代码
}
catch(Exception_2 e2){
//处理Exception_2的代码
}
……
catch(Exception_n en){
//处理Exception_n的代码
}
finally{
//通常是释放资源的代码
}
其中catch和finally都是可以默认的(即可以不写),但是不允许同时默认catch和finally。
try语句块
:该语句块中是程序正常情况下应该要完成的功能,而这些代码中可能会产生异常,其后面的catch语句块就是用来捕获并处理这些异常的。
catch语句块
:该语句块用来捕获并处理try语句块中产生的异常。每个catch语句块声明其能处理的一种特定类型的异常,catch后面的括号中就是该特定类型的异常;不过,在Java7以前,每个catch语句块只能捕获一种异常,但是,从Java7开始就支持一个catch捕获多种异常,多个异常之间用|
隔开。写法如下:
try{
//可能会产生异常的代码
}
catch(Exception_1 | Exception_2 | Exception_3 | ... | Exception_n e1){
//统一处理Exception_1、Exception_2、Exception_3、...、Exception_n的代码
}
finally{
//通常是释放资源的代码
}
catch语句块中声明的异常对象(catch(Exception_n e)
)封装了该异常的信息,可以通过该异常对象的一些方法来获取这些信息:
String getMessage()
:用来获取有关异常事件的详细信息。
void printStackTrace()
:用来跟踪异常事件发生时执行堆栈的内容。
finally语句块
:该语句块一般用于释放资源等操作。无论try语句块中是否有异常,finally语句块都会执行。
try-catch-finally执行流程(最基本的情况)
这里先介绍一下try-catch-finally执行流程最基本的情况,所谓最基本的情况就是没有用到throw关键字(抛出异常,后面会讲到)、return关键字等。
首先,执行try中的代码,如果 try中的代码没有发生异常,那么catch中的代码就不执行,等try中的代码执行完毕后直接执行finally中的代码;如果 try中的代码发生了异常(假设发生异常的代码语句是xxx),那么try中xxx下面的代码就不会执行了,会立即跳转到catch中去匹配异常,若匹配到了对应的catch中声明的异常对象,那么就执行该catch语句块中的代码,并且后面的catch语句块就不再执行。等catch语句块中的代码执行完毕,就执行finally语句块中的代码。此时,整个try-catch-finally才算执行完毕。
声明抛出异常
声明抛出异常的思路是当前方法不知道如何去处理这类异常,希望交给上一级调用者去处理,以此类推;如果主函数也不知道如何去处理这类异常,则交给jvm去处理;jvm处理异常的做法是:打印该异常的跟踪栈信息,并结束程序的执行。
声明抛出异常是一个子句,它只能写在方法头部的后面,其格式如下:
throws <异常列表>
举例:public void fun() throws IOException{......}
若在一个方法中声明抛出异常,那么调用该方法的调用者就必须对该异常进行处理,调用者处理该异常有两种方式:
- 使用try-catch-finally来捕获并处理该异常。
- 继续抛出,留给后面的调用者去处理,从而形成异常处理链。
抛出异常
上面讲到的声明抛出异常只是告诉方法的调用者要去处理异常,这只是个说明性的语句,因为方法的代码中可能有异常,也可能没有异常(没有异常时就不会抛出)。
而真正抛出异常的语句是throw <异常对象>
,其中的异常对象必须是Throwable或其子类对象。注意,throw语句抛出的不是一个异常类,而是一个异常实例。例如:
throw new Exception("这是一个异常对象!")
当执行到上述语句时,会立即结束方法的执行。
- 如果throw抛出的是一个Checked异常,那么必须处理该异常:要么该throw语句处于try代码块中,显式捕获;要么在该throw语句所处的方法中声明抛出异常,交由上一级调用者去处理。
- 如果throw抛出的是一个Runtime异常,则可以不用任何处理,交由上一级调用者去处理;也可以像上一种情况一样处理。
接下来通过案例来讲解,请看下面的代码:
@Test
public void test1(){
try{
int a=1/0; //发生异常的代码
System.out.println("算术异常!!!");
}
catch(ArithmeticException | NumberFormatException e){
System.out.println(e.getMessage()); //打印异常信息,并没有抛出异常
throw new ArithmeticException("抛出算术异常!!!"); //抛出算术异常
}
finally{
System.out.println("执行finally代码块!!");
}
System.out.println("已经抛出算术异常!!!");
}
上述代码的运行结果如下图所示:
try中的int a=1/0;
语句发生了算术异常(除数为0),直接跳转到catch中去匹配异常对象,匹配到了ArithmeticException异常对象,然后执行catch中的代码:打印异常信息,抛出异常。但是,在catch中抛出异常之前,会先执行finally中的代码,等finally中的代码执行完毕,再回到catch中抛出异常,而catch中的异常抛出后,整个方法就结束了,方法中剩下的代码就不执行了。
以上就是上述代码的完整执行流程。
Java7增强的throw语句
在Java7之前,如果在一个方法中捕获throw Exception(“xxx”);异常,由于该语句抛出的是Exception异常,故而Java编译器认为这段代码有可能会抛出Exception异常,所以必须在这段代码所处的方法中声明抛出一个Exception异常,这种方法比较简单粗暴。
而在Java7及以上版本,Java编译器会进行更加细致的检查,Java编译器会检查throw语句抛出异常的实际类型,所以,可以在方法声明抛出一个具体的异常,而不仅仅写一个Excepton异常。
异常链
真实的企业级应用通常都是分层的,例如:表现层、中间层和持久层。那么如果在持久层查询数据时抛出了SQLException异常,我们不应该把这个异常信息直接传到表现层,而是应该在中间层把原始异常记录下来,交给管理员处理,并抛出一个新的业务异常传到用户。例如下代码:
这种把捕获一个异常然后接着抛出另一一个异常,并把原始异常信息保存下来是一种典型的链式处理(23种设计模式之一:职责链模式),也被称为“异常链”。
在JDK 1.4以前,程序员必须自己编写代码来保持原始异常信息。从JDK 1.4以后,所有Throwable的子类在构造器中都可以接收-一个 cause 对象作为参数。这个cause就用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,你也能通过这个异常链追踪到异常最初发生的位置。例如希望通过上面的SalException去追踪到最原始的异常信息,则可以将该方法改写为如下形式:
异常屏蔽问题
如果在try-catch-finally代码块中,try、catch以及finally中都有异常抛出,那么最终只能抛出finally代码块中的异常。这就是异常屏蔽问题。
请看下面的代码:
//如果try、catch以及finally中都有异常抛出,那么最终只能抛出finally中的异常,try和catch中的异常抛出会被屏蔽
@Test
public void test4(){
try{
System.out.println("try");
throw new RuntimeException("try中抛出异常!!!");
}
catch(Exception e){
System.out.println("catch");
throw new RuntimeException("catch中抛出异常!!!");
}
finally{
System.out.println("finally");
throw new RuntimeException("finally中抛出异常!!!");
}
}
上述代码的运行结果为:
首先执行try中的代码:打印“try”
;然后在try中抛出异常,那么在try中的异常抛出之前,要去catch中匹配异常对象并执行catch代码块,于是,匹配到了Exception对象并执行catch中的代码:打印“catch”
,然后抛出异常;那么,在抛出catch中的异常之前,要先执行finally中的代码:打印“finally”
,并抛出finally中的异常
,此时整个方法执行结束,前面的两个异常不会被抛出。这就是所谓的异常屏蔽。
其他问题
如果在tr-catch-finally代码块中有return,那么执行流程又是怎样的呢?
请看下面的代码:
public int test5(){
try{
int a=1/0; //发生异常的代码
return 1;
}
catch(Exception e){
System.out.println("catch");
return 2;
}
finally{
System.out.println("finally");
return 3; //禁止在finally中使用return语句,这里只是举例说明,在实际开发中禁止使用
}
}
在main方法中执行上述方法,结果为:
try中int a=1/0;
语句发生了异常,那么就跳转到catch中去匹配并执行catch中的代码,而catch中要返回整数2,在执行catch中的return 2;
语句之前,要先去执行finally中的代码,因为finally中有return 3;
语句,所以当执行到finally中的return语句后,整个方法就结束了,catch以及try中的return语句就不会执行了。
请继续看下面的代码:
public int test8(){
// try中没有异常发生
try{
int a=1/1;
return 1;
}
catch(Exception e){
System.out.println("catch");
// throw new RuntimeException("catch抛出异常!!!");
return 2;
}
finally{
System.out.println("finally");
return 3; //禁止在finally中使用return语句,这里只是举例说明,在实际开发中禁止使用
}
}
上述代码的返回的结果依旧是3。
public int test8(){
// try中没有异常发生
try{
int a=1/1;
throw new Exception("try中的异常");
}
catch(Exception e){
System.out.println("catch");
return 2;
}
finally{
System.out.println("finally");
return 3; //禁止在finally中使用return语句,这里只是举例说明,在实际开发中禁止使用
}
}
上述代码的运行结果如下图所示:
try中没有发生异常的代码,但try中抛出了异常,那么在抛出try中的异常之前,会先去匹配catch声明的异常对象并执行catch中的代码,因为catch中有return语句,那么在执行catch中的return语句之前,会先去执行finally中的代码,因为finally代码块中有return语句,所以当finally中的return语句执行完毕后,整个方法就结束了。前面的return语句就不会被执行了。
综合上面的几段代码,我们来总结一下:
- 当try、catch以及finally中都抛出了异常时,只有finally中的异常会被抛出,try和catch中的异常会被屏蔽。
- 当try、catch以及finally中都有return语句时,只有finally中的return语句会被执行,try和catch中的return语句不会被执行。
- return和抛出异常(throw new XXXException())是不能同时出现在 同一个 代码块中的。
- 如果try中有return(throw)语句并且try中没有异常,那么在执行try中的return(throw)语句之前,要先去执行finally中的语句;同理,如果catch中有return(throw)语句,那么在执行catch中的return(throw)语句之前,要先去执行finally中的代码。
- 一旦执行了return语句(或throw抛出异常代码),那么该方法就立即结束了。
- 如果,在try、catch以及finally中混合了return语句和抛出异常代码(throw),执行原理请参照第4条结论。
一句话总结一下:如果try、catch和finally中都有return语句(或者都抛出了异常),那么只有finally中的return语句会执行(或者只有finally中的异常会被抛出),try和catch中的return语句不会被执行(或者try和catch中的异常不会被抛出)。
自定义异常类
自定义异常类必须是Throwable类的子类,通常是从Exception及其子类来继承。定义异常类时,通常需要提供两个构造器,一个是无参构造器,另一个是有参构造器,此参数是一个String类型的,用于描述该自定义异常的信息。例如,下述的MyException类就是一个自定义类。
// 自定义异常类,必须是Throwable的子类,通常继承自Exception及其子类
public class MyException extends Exception {
//无参构造
public MyException() {
}
public MyException(String msg) {
//调用父类的有参构造
super(msg);
}
}