Java异常处理的最佳实践

本文是关于 Exception 处理的一篇不错的文章,从 Java Exception 的概念介绍起,依次讲解了 Exception 的类型(Checked/Unchecked),Exception 处理的最佳实现:

  1. 选择 Checked 还是 Unchecked 的几个经典依据
  2. Exception 的封装问题
  3. 如无必要不要创建自己的 Exception
  4. 不要用 Exception 来作流程控制
  5. 不要轻易的忽略捕获的 Exception
  6. 不要简单地捕获顶层的 Exception
Best Practices for Exception Handling
By Gunjan Doshi 11/19/2003
原文链接:http://www.onjava.com/pub/a/o...

关于异常处理的问题之一就是要知道何时(when)和如何(how)使用它。在本文中我将介绍一些关于异常处理的最佳实践,同时我也会总结最近关于 checked Exception 使用问题的一些争论。

作为程序员,我们都希望能写出解决问题并且是高质量的代码。不幸的是,异常是伴随着我们的代码产生的副作用(side effects)。没有人喜欢副作用(side effects),所以我们很快就找到(find)了我们自己的方式来避免它,我曾经看到一些聪明的程序员用下面的方式来处理异常:

public void consumeAndForgetAllExceptions() {
    try {
        //...some code that throws exceptions
    } catch (Exception ex){
        ex.printStacktrace();
    }
}

上边的代码有什么问题么?
一旦抛出异常,正常的程序执行流程被暂停并且将控制交给catch块,catch块捕获异常并且只是 suppresses it(在控制台打印出异常信息),之后程序继续执行,从表面上看就像什么都没有发生过一样……

那下面的这种方式呢?

public void someMethod() throws Exception { }

他的方法体是空的,它不实现任何的功能(没有一句代码),空白方法怎么(how)会(can)抛出异常?JAVA并不阻止你这么做。最近,我也遇到类似的代码,方法声明中会抛出异常,但是没有实际发生(generated)该异常的代码。当我问程序员为什么要这样做,他回答说“我知道这样会影响API,但我已经习惯了这样做而且它很有效。”

C++社区曾经花了数年时间来决定(decide)如何使用异常,关于此类的争论在 java社区才刚刚开始。我看到许多Java程序员艰难(struggle)的使用异常。如果没有正确使用,异常会影响程序的性能,因为它需要使用内存和CPU来创建,抛出以及捕获异常。如果过分的依赖异常处理,会使得代码难以阅读,并使使用API的程序员感到沮丧,我们都知道这将会带来代码漏洞(hacks)和代码异味(code smells),
客户端代码可以通过忽略异常或抛出异常来避开这个问题,如前两个示例所示。

异常的本质

从广义上讲,有三种不同的情景会导致异常的抛出:

  1. 编程错误导致的异常 (Exception due Programming errors):这一类的异常是因为编程错误发生的,(如NullPointerExceptionIllegalArgumentException),客户端通常无法对这些编程错误采取任何措施。
  2. 客户端代码错误导致异常(Exceptions due to client code errors):客户端代码试图调用API不允许的操作,从而违反了合约。如果异常中提供了有用的信息,客户端可以通过其采用一些替代方法。例如:当解析格式不正确的XML文件时会抛出异常。该异常中包含导致问题发生的XML内容的具体位置。客户端可以通过这些信息采取恢复措施。
  3. 资源失效导致的异常(Exceptions due to resource failures):当资源失效时发生的异常。如内存不足或网络连接失败。客户端对资源失效的回应是要根据上下文来决定的。客户端可以在一段时间之后重试该操作,或是只记录资源失效日志并停止应用程序。

Java 异常类型

Java 定义了两类异常:

  1. 检查型异常 (Checked exceptions):从 Exception 类继承的异常都是检查型异常(checked exceptions),客户端必须处理API抛出的这类异常,通过catch子句捕获或是通过throws子句继续抛出(forwarding it outward)。
  2. 非检查型异常 (Unchecked exceptions):RuntimeException 也是 Exception 的子类,然而,从RuntimeException 继承的所有异常都会得到特殊处理。客户端代码不需要专门处理这类异常,因此它们被称为 Unchecked exceptions.
    举例来说,下图为 NullPointerException 的继承关系。
    Java异常处理的最佳实践
    图中,NullPointerException 继承自 RuntimeException,所以它是 Unchecked exception.

我见过大量使用 checked exceptions 只在极少数时候使用 Unchecked exceptions。最近,Java社区关于 checked exceptions 及其真正价值进行了热烈讨论,争论源于Java似乎是第一个带有 checked exceptions 的主流<abbr title="面向对象(Object Oriented)">OO</abbr>语言,而C++和C#根本没有 checked exception,它们所有的异常都是unchecked .

从低层抛出的 checked exception 强制要求调用方捕获或是抛出该异常。一旦客户端不能有效地处理这些被抛出的异常,API和客户端之间的异常协议(checked exception contract)就会变成不必要的负担。客户端的程序员可以通过将异常抑制(suppressing)在一个空的catch块中或是直接抛出它。从而又将这个负担交给了客户端的调用者。

Checked exception还被指责可能会破坏封装,看下面的代码:

public List getAllAccounts() throws
    FileNotFoundException, SQLException{
    ...
}

getAllAccounts() 方法抛出了两个检查型异常。调用此方法的客户端必须明确的处理这两种具体的异常,即使它并不知道在 getAllAccounts() 中哪个文件或是数据库调用失败了,
或者没有提供文件系统或数据库逻辑的业务,因此,这样的异常处理导致方法和调用者之间不当的强耦合(tight coupling)。

设计异常的最佳实践 (Best Practises for Designing the API)

在讨论了这些之后,现在让我们来探讨一下如何设计一个正确抛出异常的API。

1. 当要决定是采用 checked exceptions 还是 unchecked exceptions 的时候,问自己这样的一个问题,“如果这种异常一旦抛出,客户端会进行怎样的处理?”

如果客户端可以采取措施从异常中恢复,那就选择 checked exception 。如果客户端不能采取有效的措施,就选择 unchecked exceptions 。有效的措施是指从异常中恢复的措施,而不仅仅是记录异常日志。总结一下:

Client's reaction when exception happensException type
Client code cannot do anythingMake it an unchecked exception
Client code will take some useful recovery action based on information in exceptionmake it a checked exception

此外,尽量使用 unchecked exception 来处理编程错误:unchecked exception 的优点在于不强制客户端显示的处理它,它会传播(propagate)到任何你想捕获它的地方,或者它会在出现的地方挂起程序并报告异常信息。Java API中提供了丰富的 unchecked excetpion,如:NullPointerException , IllegalArgumentExceptionIllegalStateException 等。我更倾向于使用JAVA提供的标准异常类而不愿创建新的异常类,这样使我的代码易于理解并避免过多的消耗内存。

2. 保护封装性 (Preserve encapsulation)

永远不要让特定于实现的 checked exception 传递到更高层,比如,不要将数据访问层的 SQLException 传递到业务层,业务层并不需要了解(不关心? ) SQLException ,你有两种方法来解决这种问题:

  • 如果需要客户端代码从异常中恢复,则将 SQLException 转换为另一个 checked exception 。
  • 如果客户端代码无法对其进行处理,请将 SQLException 转换为 unchecked exception 。

大多数情况下,客户端代码都是对 SQLException 无能为力的,不要犹豫,把它转换为一个 unchecked exception ,考虑以下代码:

public void dataAccessCode(){
    try{
        //...some code that throws SQLException
    }catch(SQLException ex){
        ex.printStacktrace();
    }
}

这里的catch块仅仅打印异常信息而没有任何的直接操作,这样做的理由是客户端无法处理 SQLException (但是显然这种就象什么事情都没发生一样的做法是不可取的),不如通过如下的方式解决它:

public void dataAccessCode(){
    try{
       //...some code that throws SQLException
    }catch(SQLException ex){
        throw new RuntimeException(ex);
    }
}

这里将 SQLException 转化为了 RuntimeException,一旦SQLException被抛出,catch块就会抛出一个RuntimeException,当前执行的线程将会停止并报告该异常。
但是,该异常并没有影响到我的业务逻辑模块,它无需进行异常处理,更何况它根本无法对SQLException进行任何操作。如果我的catch块需要根异常原因,可以使用从JDK1.4开始所有异常类中都有的getCause()方法。
如果你确信在SQLException被抛出时业务层可以执行某些恢复操作,那么你可以将其转换为一个更有意义的 unchecked exception 。但是我发现在大多时候抛出RuntimeException已经足够用了。

3. 当无法提供更加有用信息时,不要自定义异常 (Try not to create new custom exceptions if they do not have useful information for client code.)

以下代码有什么问题?

public class DuplicateUsernameException
    extends Exception {}

它除了有一个“意义明确”(indicative exception)的名字以外,它没有给客户端代码提供任何有用的信息。不要忘记 Exception 跟其他的Java类一样,你可以添加你认为客户端代码将调用的方法供客户端调用,以获得有用的信息。
我们可以为 DuplicateUsernameException 添加一些必要的方法,如下:

public class DuplicateUsernameException
    extends Exception {
    public DuplicateUsernameException 
        (String username){....}
    public String requestedUsername(){...}
    public String[] availableNames(){...}
}

新版本提供了两个有用的方法: requestedUsername(),它会返回请求的名称。availableNames(),它会返回一组与请求类似的可用的usernames。客户端可以使用这些方法来告知所请求的用户名不可用,其他用户名可用。但是如果你不准备添加这些额外的信息,那么只需抛出一个标准的Exception:

throw new Exception("Username already taken");

如果你认为客户端代码除了记录已经采用的用户名之外不会进行任何操作,那么最好抛出 unchecked exception :

throw new RuntimeException("Username already taken");

另外,你可以提供一个方法来验证该username是否被占用。

很有必要再重申一下,在客户端API可以根据异常信息进行某些操作的情况下,将使用 checked exception 。
处理程序中的错误更倾向于用 unchecked excetpion (Prefer unchecked exceptions for all programmatic errors)。它们使你的代码更具可读性。

4. 文档化异常 (Document exceptions)

你可以使用 Javadoc 的 @throws 标签来说明(document)你的API中要抛出 checked exception 或者 unchecked exception。然而,我更倾向于使用来单元测试来文档化异常(document exception)。单元测试允许我在使用中查看异常,并且作为一个可以被执行的文档来使用。不管你采用哪种方式,你要让客户端代码知道你的API中所要抛出的异常。这是一个用单元测试来测试IndexOutOfBoundsException的例子: 这里提供了IndexOutOfBoundsException的单元测试。

public void testIndexOutOfBoundsException() {
    ArrayList blankList = new ArrayList();
    try {
        blankList.get(10);
        fail("Should raise an IndexOutOfBoundsException");
    } catch (IndexOutOfBoundsException success) {}
}

上面这段代码在调用 blankList.get(10) 应当抛出 IndexOutOfBoundsException 。如果没有抛出该异常,则会执行 fail("Should raise an IndexOutOfBoundsException") 显式的说明该测试失败了。通过为异常编写单元测试,你不仅可以记录异常如何触发,还可以使你的代码在经过这些测试后更加健壮。

使用异常的最佳实践 (Best Practices for Using Exceptions)

下一组最佳实践展示了客户端代码应如何处理抛出 checked exception 的API。

1. 总是要做一些清理工作 (Always clean up after yourself)

如果你在使用如数据库连接或是网络连接之类的资源,请记住要做一些清理工作 (如关闭数据库连接或者网络连接),如果你调用的API仅抛出 Unchecked exception ,你应该在使用后用try - finally块清理资源。

public void dataAccessCode(){
    Connection conn = null;
    try{
        conn = getConnection();
        //...some code that throws SQLException
    }catch(SQLException ex){
        ex.printStacktrace();
    } finally{
        DBUtil.closeConnection(conn);
    }
}

class DBUtil{
    public static void closeConnection
        (Connection conn){
        try{
            conn.close();
        } catch(SQLException ex){
            logger.error("Cannot close connection");
            throw new RuntimeException(ex);
        }
    }
}

DBUtil 类关闭 Connection 连接,这里的重点在于 finally 块,不管程序是否碰到异常,它都会被执行。在上边的例子中,在 finally 中关闭连接,如果在关闭连接的时候出现错误就抛出 RuntimeException

2. 不要使用异常来控制流程 (Never use exceptions for flow control)

生成堆栈跟踪 (stack trace) 的代价很昂贵,堆栈跟踪的价值在于debug中使用。在一个流程控制中,堆栈跟踪应当被忽视,因为客户端只想知道如何进行。

在下面的代码中,MaximumCountReachedException 被用来进行流程控制:

public void useExceptionsForFlowControl() {
    try {
        while (true) {
            increaseCount();
        }
    } catch (MaximumCountReachedException ex) {
    }
    //Continue execution
}

public void increaseCount()
    throws MaximumCountReachedException {
    if (count >= 5000)
        throw new MaximumCountReachedException();
}

useExceptionsForFlowControl() 用一个无限循环来增加count直到抛出异常,这种方式使得代码难以阅读,而且影响代码性能。只在要会抛出异常的地方进行异常处理。

3. 不要忽略异常 (Do not suppress or ignore exceptions)

当API中的方法抛出 checked exception 时,它在提醒你应当采取一些措施。如果 checked exception 没有任何意义,请毫不犹豫的将其转化为 unchecked exception 再重新抛出。而不是用一个空的 catch 块捕捉来忽略它,然后继续执行,以至于从表面来看仿佛什么也没有发生一样。

4. 不要捕获顶层的Exception (Do not catch top-level exceptions)

unchecked exception 都是 RuntimeException 的子类,而 RuntimeException 又继承自 Exception,如果单纯的捕获 Exception , 那么你同样也捕获了 RuntimeException ,如以下代码所示:

try{
    // ...
}catch(Exception ex){
}

上边的代码(注意catch块是空的)将忽略所有的异常,包括 unchecked exception .

5. 只记录异常一次 (Log exceptions just once)

将相同的异常多次记入日志会使得检查追踪栈的开发人员感到困惑,不知道何处是报错的根源。所以只记录一次。

Summary

These are some suggestions for exception-handling best practices. I have no intention of staring a religious war on checked exceptions vs. unchecked exceptions. You will have to customize the design and usage according to your requirements. I am confident that over time, we will find better ways to code with exceptions.

I would like to thank Bruce Eckel, Joshua Kerievsky, and Somik Raha for their support in writing this article.

相关推荐