在 Java 中,异常对象都是派生于 Throwable 类的一个实例。如果 Java 中内置的异常类不能满足需求,用户还可以创建自己的异常类。
派生于 Error类 或 RuntimeException类 的所有异常称为 非受查异常(unchecked);所有其他的异常称为 受查异常(checked),这是可以在编译阶段被处理的异常,所以它强制程序处理所有的 Checked异常。
Error 错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。应用程序无须处理这些错误。
使用 try…catch 捕获异常
1 | try |
如果执行 try 块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给 Java 运行时环境,这个过程被称为 抛出(throw)异常。
当 Java 运行时环境收到异常对象时,会寻找能处理该异常对象的 catch 块,如果找到合适的 catch 块,则把异常对象交给该 catch 块处理,这个过程被称为捕获(catch)异常;如果Java运行时环境找不到捕获异常的 catch 块,则运行时环境终止,Java程序也将退出。
不管程序代码是否处于 try 块中,甚至包括 catch 块中的代码,只要执行该代码块时出现了异常,系统总会产生一个异常对象。如果程序没有为这段代码定义任何的 catch 块,则Java运行时环境无法找到处理该异常的 catch 块,程序就在此退出。
在进行异常捕获时应该把所有父类异常的 catch 块排在子类异常 catch 块的后面,简称:先处理小异常,再处理大异常。通常情况下,如果 try 块被执行一次,则只有一个 catch 块会被执行,绝不可能有多个 catch 块被执行。
Java7提供的多异常捕获
从Java7开始,一个 catch 块可以捕获多种类型的异常,但需要注意两个地方:
- 捕获多种类型的异常时,多种异常类型之间用竖线 (|) 隔开。
- 捕获多种类型的异常时,异常变量有隐式的 final,因此程序不能对异常变量重新赋值。
以下程序示范了多异常捕获:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21try
{
int a=Integer.parseInt(args[0]);
int b=Integer.parseInt(args[1]);
int c=a/b;
System.out.println("您输入的两个数相除的结果是:"+ c);
}
catch(IndexOutOfBoundsException | NumberFormatException | ArithmeticException e)
{
System.out.println("程序发生了数组越界、数字格式异常、算数异常之一");
//捕获多异常时,异常变量默认有 final 修饰
//所以下面代码会出错
e=new ArithmeticException("test");
}
catch(Exceptioon e)
{
System.out.pritnln("未知异常");
//捕获一种未知的异常,该异常变量没有 final 修饰
//所以下面代码正确
e=new RuntimeException("test);
}
访问异常信息
如果程序需要在 catch 中访问异常对象的相关信息,则可以通过访问 catch 块括号里面的异常形参来获得,所有的异常对象都包含了如下几个常用方法:
- getMessage():返回该异常的详细描述字符串。
- printStackTrace():将该异常的跟踪栈信息输出到标准错误输出。
- printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流。
- getStackTrace():返回该异常的跟踪栈信息。
使用 finally 回收资源
因为 Java 的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所占用的内存,所以程序在 try 块里打开的物理资源(如数据库连接、网络连接和磁盘文件),必须要显式回收。
除非在 try 块、catch 块中调用了退出虚拟机的方法,否则不管在 try 块、catch 块中执行怎样的代码,出现怎样的情况,异常处理的 finally 块总会被执行。
因此,当 Java 程序执行 try 块、catch 块时遇到了 return 或 throw 语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句不会结束该方法,而是去寻找该异常处理流程中是否包含 finall 块,如果没有 finall 块,程序立即执行 return 或 throw 语句,方法终止;如果有 finally 块(finally中没有return或throw),系统立即开始执行 finall 块。当 finall 块完成后,系统才会再次跳转回来执行 try块、catch块里的 return 或 throw 语句。如果 finall 块里也使用了 return 或 throw 等导致方法终止的语句,将会导致 try块、catch块 中的 return、throw 语句失效。
自动关闭资源的 try 语句
从 Java7 开始,允许在 try 关键字后紧跟一堆圆括号,圆括号内可以声明、初始化一个或多个资源(资源指的是必须在程序结束时显式关闭的资源,比如数据库连接、网络连接等),try 语句在该语句结束时自动关闭这些资源。但这些资源实现类必须实现 AutoCloseable 或 Closeable 接口,实现这两个接口就必须实现 close()方法。
Closeable 是 AutoCloseable 的子接口。Closeable 接口里的 close() 方法声明抛出了 IOException,因此它的实现类在实现 close() 方法时只能声明抛出 IOException 或其子类;AutoCloseable 接口里的 close() 方法声明抛出了 Exception ,因此它的实现类在实现 close() 方法时可以声明抛出任何异常。
使用 throws 声明抛出异常
使用 throws 声明抛出异常的思路是,当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果 main 方法也不知道如何处理这种类型的异常,也可以使用 throws 声明抛出异常,该异常将交给 JVM 处理。JVM 对异常的处理方法是:打印异常的跟踪栈信息,并终止程序运行。
如果某段代码中调用了一个带 throws 声明的方法,该方法声明抛出了 Checked异常,则表明该方法希望它的调用者来处理该异常。也就是说,调用该方法时要么放在try 块中显式捕获该异常,要么放在另一个带 throws 声明抛出的方法中。
使用 throws 声明抛出异常时有一个限制:子类方法声明抛出的异常类型应该时父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
使用 throw 抛出异常
Java允许程序自行抛出异常,使用 throw 语句来完成。1
throw ExceptionInstance;
当 Java 运行时接收到开发者自行抛出异常时,同样会中止当前的执行流,跳到该异常对应的 catch 块,由该 catch 块来处理该异常。
自定义异常类
自定义异常应该继承 Exception 类,或者继承 Exception 类的子类。自定义异常应该包含两个构造器:一个是无参数的默认构造器;另一个是带有详细描述信息字符串的构造器,这个字符串将作为异常对象的 getMessage() 方法的返回值。1
2
3
4
5
6
7
8
9
10
11public class MyException extends Exception
{
//无参数构造器
public MyException(){}
//带有字符串参数的构造器
public MyException(String msg)
{
super(msg);
}
}
catch 和 throw 同时使用
在实际应用中对异常的处理往往更复杂,当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可以完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获异常。
为了实现这种通过多个方法协作处理同一个异常的情形,可以在 catch 块中结合 throw 语句来完成:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//声明要抛出的异常 anotherException
public void method() throws anotherException
{
try
{
throw Exception();
}
catch(Exception e)
{
//此处完成本方法中可以对异常执行的修复处理
e.printStackTrace();
//再次抛出同一个异常
throw new anotherException();
}
}
这种 catch 和 throw 结合使用的情况可以用在:
① 应用后台需要通过日志来记录异常发送的详细情况。
② 应用还需要根据异常向应用使用者传达某种提示。
Java7增强的 throw 语句
先看这一段代码:1
2
3
4
5
6
7
8
9try
{
throw new FileOutputStream("a.txt");
}
catch(Exception ex)
{
ex.printStackTrace();
trow ex; //① 这里再次抛出捕获到的异常
}
上面的代码在 catch 块中再次抛出了捕获到的异常,但这个 ex 对象比较特殊:程序捕获该异常时,声明该异常的类型为 Exception,但实际上 try 块中可能只调用了 FileOutputStream 构造器,这个构造器只是声明抛出了 FileNotFoundException 异常。利用 Exception 捕获,显然是大材小用。
从 Java7 开始,Java 编译器会执行更细致的检查,Java 编译器会检查 throw 语句抛出异常的实际类型,这样编译器知道 ① 代码处实际抛出的是 FileNotFoundException 异常。因此在调用该段代码的方法签名中只需要声明抛出 FileNotFoundException 异常即可。
异常链
在真实的应用程序中,有严格的分层关系,层与层之间有非常清晰的划分,上层功能的实现严格依赖于下层的 API,也不会跨层访问。
对于上图所示结构的应用,当业务逻辑层访问持久层出现 SQLException 异常时,程序不应该把底层的 SQLException 异常传到用户界面。通常的做法是:
程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为 异常转译。
假设程序需要实现工资计算的方法,则程序应该采用如下结构的代码实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public void calSalary() throws SalaryException
{
try
{
//实现结算工资的业务逻辑
.......
}
catch(SQLException sqle)
{
//把原始异常记录下来,留给管理员
....
//下面异常中的message就是对用户的提示
throw new SalaryException("访问底层数据库出现异常");
}
catch(Exception e)
{
//把原始异常记录下来,留给管理员
....
//下面异常中的message就是对用户的提示
throw new SalaryException("系统出现未知异常");
}
}
以上代码把原始异常信息隐藏起来,仅向上提供必要的异常提示信息的处理方式,可以保证底层异常不会扩散到表现层,可以避免向上暴露太多的实现细节,这符合面向对象的封装原则。
这种把捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理,也被称为 “异常链”。
所有 Throwable 的子类在构造器中都可以接收一个 cause 对象作为参数。这个 cause 就用来表示原始异常,这样就可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,你也能通过这个 异常链 追踪到异常最初发生的位置。
例如希望通过上面的 SQLException 去追踪到最原始的异常信息,则可以将方法改写:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public void calSalary() throws SalaryException
{
try
{
//实现结算工资的业务逻辑
.......
}
catch(SQLException sqle)
{
//把原始异常记录下来,留给管理员
....
//下面异常中的message就是对用户的提示
throw new SalaryException(sqle); //此处不同
}
catch(Exception e)
{
//把原始异常记录下来,留给管理员
....
//下面异常中的 e 就是对用户的提示
throw new SalaryException(e);
}
}
上面的代码在创建 SQLException 对象时,传入了一个 Exception 对象,而不是传入了一个 String 对象,这就需要 SQLException 类有对应的构造器:1
2
3
4
5
6
7
8
9
10
11
12public class SalaryException extends Exception
{
public SalaryException(){}
public SalaryException(String msg){}
//创建一个可以接收 Throwable 参数的构造器
public SalaryException(Throwable t)
{
super(t);
}
}
创建了这个 SalaryException 业务异常类后,就可以用它来封装原始异常,从而实现对异常的链式处理。
异常处理规则
成功的异常处理规则应该实现如下 4 个目标:
- 使程序代码混乱最小化。
- 捕获并保留诊断信息。
- 通知合适的人员。
- 采用合适的方式结束异常活动。
要达到以上目标,要做到:
- 不要过度使用异常。
- 不要使用过于庞大的 try 块。
- 避免使用 Catch All 语句。
- 不要忽略捕获到的异常。