怎样进行异常处理呢?
引言
“如何更好地使用Java异常处理机制”。这是一个老生常谈,仁者见仁,智者见智的问题,笔者今天将根据自身与他人在实践当中的经验与解决方案进行总结,提出一些个人见解。为了篇幅的考虑,本文将假设所有读者对Java异常处理机制有相应的了解,对于还未接触过Java异常处理机制的朋友,可以参阅《The Java Tutorial》、《Thinking in Java》相应的章节。因文笔、技术等能力有限,此文无法避免的出现描述不当、遗漏或错误,如有发现,还请各位看官多多指点与海涵。
关于异常
1. 异常的基本概念
在展开论述前,首先一起探讨一下异常(exception)的定义,在《The Java Tutorial》是这样定义的:异常是程序在执行时发生的事件,它将会打断指令的正常流程。在Java中,异常的结构请见图一:

图一 Java 异常结构图
而根据处理类型分类可以分为 :
- 未受检异常(unchecked)
编译器不要求强制处置的异常,其中RuntimeException与其子类就属于此类异常。
- 受检异常(checked)
编译器要求必须处置的异常,如Exception、IOException等。
2. 关于“高级异常”与“低级异常”
针对业务系统或应用程序而言,异常应该有一个级别的概念,级别越高的异常越其表达出来的含义,应该越符合系统或应用程序对应现实世界中的具体业务;反之,级别越低的异常将更符合计算机的表达方式。
比如,一位用户去ATM机取钱,当他遇到“余额已不足”异常时,此异常时用户可以理解的,这种异常的级别相应位高级的;当系统抛出“被减数小于零”异常时,这与用户取钱这项行为几乎无关,对用户而言,也无法理解,对应的这种异常的级别相应较低。在此,为了更好的进行下文描述,本人将在此约定命名,级别高的异常为“高级异常”,而级别低的,称之为“低级异常”。
异常处理
1. 异常处理的目标
在进行异常处理前,我们必须有明确的目标,我们应该对异常处理机制所能达到的效果,有一个清晰的描述。因此,在进行异常处理时,应满足以下几个要求:
a) 根据不同用户,给出相应能够理解的异常信息。
b) 异常信息绝对不能丢失,并且还需较详细的状态描述和堆栈信息。
c) 异常层次分明,职责分明。
在满足以上几点的同时,异常处理的代码应尽可能做到美观,增强程序代码的可读性。
2. 一些异常处理的示例
在上一节,我们有了一个比较清晰的异常处理目标,接下来我们将看看一些示例,看是否能达到我们的目标。
a) 忽略异常
try {
doSomething();
} catch (SomeException e) {}
点评:直接将异常信息全给“吃”了,并没有给出任何提示,这完全就是掩耳盗铃的行为。如果非得存在,那么也应该详细的解释此异常为什么可以忽略。
b) 简单的打印堆栈。
try {
doSomething();
} catch (SomeException e) {
e.printStackTrace();
}
点评:同上,虽然将堆栈在控制台打印出来,但是这样的信息很容易就被淹没,特别是低级异常,很难得到相应的上下文信息,这势必导致异常难以定位与分析。将异常信息记录下来或许是一个不错的选择。
c) catch所有异常,而不是特定异常。
try {
doSomething();
} catch (Exception e) {
System.out.println("doSomething 执行失败。");
}
申明:为了更关注于我们的问题,在这和后面直接以System.out.println作为异常处理的动作(后面不再申明)。但实际处理中,并不建议。原因请见上一个案例 b)
点评:此段代码将捕获可能由 doSomething抛出的任何 RuntimeException 并且阻止它们进行扩散。建议根据实际需求按级别分别捕获相应的异常,非得catch Exception,那么Exception应该是最后 catch。
d) 不清晰的描述
try {
process( someFile );
} catch (Exception e) {
System.out.println("文件处理错误");
}
点评: “文件处理错误”?这里catch住所有的异常,再给出一个不清晰的描述。到底是文件不存在错误呢?还是打开流错误呢?更有甚者,此处有可能出现一个与文件没有半点关系的错误。
e) 将特定异常转成一般异常。
try {
doSomething();
} catch (SomeException e) {
throw new Exception(e);
}
点评:特定的异常是针对特定的问题而定义的,本身就包含了一定的含义 ,转换成一般异常,这仅仅利于异常处理,并不利于排错、定位。抛出一个与该级别(业务)抽象相关的异常或许会更好一点。
f) 将高级异常转成低级异常。
try {
doSomething();
} catch (HighLevelException e) {
throw new LowLevelException(e);
}
点评:我想,这个应该不需要介绍了吧。J
g) 过大的catch区域
try {
doSomething1();
doSomething2();
doSomething3();
} catch (SomeException e) {
System.out.println("异常处理");
}
点评:如果doSomething方法都有可能throw出SomeException时。这样做将不能快速定位,不得不反问到底是哪一个方法抛出的呢。
h) 大量细小的try catch块
public void process() {
try {
doSomething1();
} catch (SomeException e) {
System.out.println("异常处理");
}
try {
doSomething2();
} catch (SomeException e) {
System.out.println("异常处理");
}
try {
doSomething3();
} catch (SomeException e) {
System.out.println("异常处理");
}
}
点评:捕获了特定的异常、并且能够清晰的定位到方法,这样总无话可说了吧。没错,这样很好!但是一个逻辑处理的方法被三个try catch块切分的支离破碎,我想在阅读此方法的代码时,一眼望去,这段代码表述的是:‘逻辑处理 -> 异常处理 -> 逻辑处理……’,将try-catch块抽离出来再设定一个见名知意的方法名,会不会更好呢?
3. 异常处理时的一些建议
在实际情况中,我们应该尽量避免上面所述的异常处理示例。在实际操作中,有可能遇到更多的问题,我们不妨考虑一下如下的建议。
a) 尽可能的处理异常
要尽可能的处理异常,如果条件确实不允许,无法在自己的代码中完成处理,就考虑声明异常。如果人为避免在代码中处理异常,仅作声明,这是一种错误和依赖的实践。
b) 具体问题具体解决。
异常的部分优点在于能为不同类型的问题提供不同的处理操作。有效异常处理的关键是识别特定故障场景,并开发解决此场景的特定相应行为。为了充分利用异常处理能力,需要为特定类型的问题构建特定的处理器块。
c) 记录可能影响应用程序运行的异常
至少要采取一些永久的方式,记录下可能影响应用程序操作的异常。理想情况下,当然是在第一时间解决引发异常的基本问题。不过,无论采用哪种处理操作,一般总应记录下潜在的关键问题。别看这个操作很简单,但它可以帮助您用很少的时间来跟踪应用程序中复杂问题的起因。
d) 根据抽象(业务)将低级异常转译为高级异常
一个方法所抛出的异常应该在一个抽象层次上定义,该抽象层次与该方法做什么相一致,而不一定与方法的底层实现细节相一致。例如,一个从文件、数据库或者 JNDI 装载资源的方法在不能找到资源时,应该抛出某种 ResourceNotFound 异常(通常使用异常链来保存隐含的原因),而不是更底层的 IOException 、 SQLException 或者 NamingException 。
try {
doSomething();
} catch (LowLevelException e) {
throw new HighLevelException("doSomething执行错误。",e);
}
e) 只为异常条件使用异常
也就是说,不要为控制流使用异常,比如,在调用 Iterator.next() 时而不是在第一次检查 Iterator.hasNext() 时捕获 NoSuchElementException。
f) 为可恢复的条件使用检查型异常,为编程错误使用运行时异常
g) 避免不必要的使用检查型异常
对于调用者不可能从其中恢复的情形,或者惟一可以预见的响应将是程序退出,则不要使用检查型异常
推荐阅读
- 《改进错误处理风格》:指出系统应该抛出什么和捕获什么。
- 《技巧:当不能抛出异常时》:当既不能处理、也不能抛出 checked 异常时,有哪些选择。
- 《关于异常的争论》:到底使用受检异常还是非受检异常呢?
- 《Effective Java》:JDK5.0 Java Collections Framework的设计者Joshua Bloch 的书很好地阐述了在 Java 语言中如何进行适当的异常处理。本文很多观点直接来源于此书。
浙公网安备 33010602011771号