Java中异常处理是识别及响应错误的机制。有效地异常处理能使程序更加健壮。异常处理是非功能性需求。
异常的处理机制
Java 通过面向对象的方法进行异常处理。每个异常都是一个对象。
当程序违反了Java的语义规则时,会自动生成一个异常类对象。两种情况会生成异常类对象。一种是内置的语义检查。另一种是自定义的异常,用throw关键字引发异常。
生成的异常对象将被提交给运行时系统,此过程为抛出(throw)异常。
当运行时系统接收到异常对象时,会寻找能处理此异常的代码并交给其处理,此过程为捕获(catch)异常。
如果运行时系统找不到可以捕获异常的方法,则将终止系统,结束程序。
异常类的层次结构
Java定义了一个异常类的层次结构,以java.lang.Throwable开始,扩展出Error和Exception,Exception又扩展出RuntimeException。
RuntimeException表示的是代码所导致的问题,如数组访问越界,空指针异常,类转换异常等。而派生自Exception类的异常所表示的并不是代码本身的问题所导致的非正常状态,而是应用本身无法控制的情况。例如一个应用在尝试打开一个文件并写入的时候,该文件已经被另外一个应用打开从而无法写入。
Error是一系列很难通过程序解决的问题。这些问题基本上是无法恢复的,例如内存、堆栈空间不足等。一般情况下,不会对从Error类派生的各个异常进行处理。
java为系统异常和普通异常提供了不同的解决方案,编译器强制普通异常必须被try..catch处理或用throws声明继续抛给上层调用方法,所以普通异常也称为checked异常;而系统异常可以处理也可以不处理,所以,编译器不强制用try..catch处理或用throws声明,所以系统异常也称为unchecked异常。
关键字try,catch,throw,throws,finally
try是指定一块需要预防"异常"的程序。catch子句是指定捕捉的"异常"的类型。 throw语句是抛出"异常"。 throws是标明方法可能抛出的"异常"。finally是确保不管发生什么"异常"都被执行的语句。
每当遇到一个try语句,"异常"的框架就放到堆栈上面,直到所有的try语句都完成。如果下一级的try语句没有对某种"异常"进行处理,堆栈就会展开,直到遇到有处理这种"异常"的try语句。
在JDK7中提供了try-with-resources机制, 它规定你操作的类只要是实现了AutoCloseable接口就可以在try语句块退出的时候自动调用close 方法关闭流资源。
处理异常
三种处理异常的基本模式:转换(translate)、重试(retry)和恢复(recover)。
转换经常用于处理受检异常(checked exception),在方法中异常无法抛出,并且无法恢复时使用。 在这种情况下,将其转换为运行时异常(runtime exception)而后抛出是最合适的做法。接下来,运行时异常通常由框架处理。
在处理不可靠的服务时,重试非常有用,前提是重新尝试有意义。一个很好的例子就是网络中断重试。如果定义了这种策略,那么就能够恢复到正常状态。例如,如果通过网络发送数据失败,可以将数据写入本地存储。当然,这时就必须定义如何处理该文件。此外,上面提到的模式可以组合。
// 转换
try {
throw new IOException("Made up");
} catch (IOException e) {
throw new RuntimeException(e);
}
// 重试5次后放弃
boolean end = false;
int count = 0;
while (end == false) {
try {
// 发送信息
if (true) {
throw new MessagingException("Made up");
}
end = true;
} catch (MessagingException e) {
if (count >= 5) {
// 尝试5次放弃。
throw new RuntimeException("was not able to send message even after five tries", e);
}
++count;
try {
Thread.sleep(30000);
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
throw new RuntimeException(e1);
}
}
}
// 恢复:如果传输失败记录到文件
try {
// 发送信息
throw new MessagingException("Made up");
} catch (MessagingException e) {
try {
// 写文件
throw new IOException("Made up");
} catch (IOException e1) {
// 如果写文件失败,不再进行恢复
throw new RuntimeException(e1);
}
}
异常处理的建议
1 为可恢复的错误使用受检异常,为编程错误使用非受检异常。
受检异常保证你会针对错误情况提供异常处理代码,这是一种从语言层面上强制编写健壮代码的方式,但引入异常处理代码会导致代码可读性变差。当然,如果有可替代方式或恢复策略的话,捕获异常并做处理看起来似乎也合情合理。
例如,在批量处理任务中,如果是处理子任务时出现问题,那么只记录子任务的异常,继续处理其他的子任务。对失败的任务进行重试或者记录到日志中。
2 避免过度使用受检异常
在方法重写或接口实现中,受检异常会污染其他方法。特别是在API方法中,要慎用。
3 将受检异常转为运行时异常
这是在诸如Spring之类的框架中用来减少使用受检异常的方式之一。这样可以将特定的异常限制在特定的模块中,比如把 SQLException 抛到 DAO 层,把有意义的运行时异常抛到客户端层。
4 在 finally 程序块中关闭或者释放资源
这是 Java 编程中广为人知的最佳实践和事实上的标准,尤其是在处理网络和 IO 操作的时候。从 Java7 开始,新增加了一项功能:自动资源管理,或者称之为ARM块。尽管如此,仍然要记住在 finally 块中关闭资源,这对于释放像 FileDescriptors 这类资源至关重要。
5 避免空的 catch 块
空的 catch 块不仅隐藏了错误和异常,同时可能导致对象处于不可用状态或者脏状态。
6 提早抛出(迅速失败)
在导致错误的原始地方就抛出异常。一般是入参错误,导致其他地方抛出异常,这时应该先检查入参,如果有问题即刻抛出异常。
7 延迟处理(throw和throws)
在有能力处理的地方,才能抛出异常。
8 日志要记录完整异常上下文
在程序执行期间,用日志记录错误依然是最好的方法。这在 Java 异常处理中不仅仅是一个最佳实践,而且是一个最通用的实践。而且,日志需要记录详细入参。什么何时何处为何。
在API中,受检异常的使用
首先,Checked Exception应当只在异常情况对于API以及API的使用者都无法避免的情况下被使用。例如在打开一个文件的时候,API以及API的使用者都没有办法保证该文件一定存在。反过来,在通过索引访问数据的时候,如果API的使用者对参数index传入的是-1,那么这就是一个代码上的错误,是完全可以避免的。因此对于index参数值不对的情况,我们应该使用Unchecked Exception。
其次,Checked Exception不应该被广泛调用的API所抛出。
再次,Checked Exception应该有明确的意义。
对于API的用户而言,一旦遇到API抛出Checked Exception,那么就需要考虑使用一个Wrapped Exception来将该Checked Exception包装起来。
参考
http://www.cnblogs.com/liaoliao/p/5009156.html
http://www.importnew.com/20139.html
dzone.com/articles/good-exception-handling
网友评论