分享

Java 异常处理的最佳实践

 集微笔记 2013-11-21

  译者注:这是一篇 2003 年的文章,因为时间久远,可能有些观点已经过时,但里面讨论的大部分方法如今仍能适用。如若有其他好的错误处理的方法,欢迎留言。

  异常处理的关键就在于知道何时处理异常以及如何使用异常。这篇文章,我会提到一些最佳的异常处理方法。我也会总结 checked exception 的用法。

  我们程序员都想写出高质量的代码来解决问题。但是,异常有时会给我们的代码带来副作用。没有人喜欢副作用,所以我们很快找到了方法来改善它们。我看见过许多聪明的程序员这样来处理异常:

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

  上面的代码有什么错误?

  当异常被抛出后,正常的程序执行过程中断,控制权交给 catch 段,catch 段会 catch 异常,然后抑制异常的进一步扩大。然后接着 catch 段之后程序继续执行,好像什么都没发生过一样。

  下面的代码呢?

public void someMethod () throws Exception{
}

  这个方法内没有代码,是个空方法。一个空方法怎么能抛出异常呢?Java 并没有说不让这么做。最近,我遇到过类似的代码,方法抛出了异常,而其中的代码实际上并不产生那个异常。当我问这个程序员为何要这么做,他回答道“我知道,虽然这样做破坏了 API,但我习惯这么做,而且这样也可行。”

  C++社区用了许多年才确定如何使用异常机制。这个争论刚刚在 Java 社区展开。我见到一些 Java 程序员正在和异常进行顽强抗争。如果用法不当的话,会拖慢程序,因为创建、抛出和接住异常都会占用内存。如果过多的使用异常的话,代码会变得很难阅读,对要使用 API 的程序员来说无疑会增加挫败感。我们知道挫败感会令我们写出很烂的代码。有的程序员会刻意回避这个问题,忽略异常或随意抛出异常,就像上面的两个例子一样。

  异常的本质

  广义的讲,抛出异常分三种不同的情况:

  - 编程错误导致的异常:在这个类别里,异常的出现是由于代码的错误(譬如 NullPointerException 和 IllegalArgumentException)。代码通常对编程错误没有什么对策。

  - 客户端的错误导致的异常:客户端代码试图违背制定的规则,调用 API 不支持的资源。如果在异常中显示有效信息的话,客户端可以采取其他的补救方法。例如:解析一个格式不正确的 XML 文档时会抛出异常,异常中含有有效的信息。客户端可以利用这个有效信息来采取恢复的步骤。

  - 资源错误导致的异常:当获取资源错误时引发的异常。例如,系统内存不足,或者网络连接失败。客户端对于资源错误的反应是视情况而定的。客户端可能一段时间之后重试或者仅仅记录失败然后将程序挂起

  Java 异常的类型

  Java 定义了两种异常

  - Checked exception: 继承自 Exception 类是 checked exception。代码需要处理 API 抛出的 checked exception,要么用 catch 语句,要么直接用 throws 语句抛出去。

  - Unchecked exception: 也称 RuntimeException,它也是继承自 Exception。但所有 RuntimeException 的子类都有个特点,就是代码不需要处理它们的异常也能通过编译,所以它们称作 unchecked exception。

  图 1 显示了 NullpointerException 的继承级别。

图 1 异常等级实例图 1 异常等级实例

  NullpointerException 继承自 RuntimeException,所以它是个 unchecked exception。

  我看到人们大量使用 checked exception 的,而很少看到 unchecked exception 的使用。近来,在 Java 社区里对 checked exception 和它的真正价值的争论愈演愈烈。这主要因为 Java 是第一个使用 checked exception 的主流面向对象语言。C++和 C# 都没有 checked exception,所有的异常都是 unchecked。

  低层次抛出的 checked exception 对高层次来说,必须要 catch 或者 throw 它们。这样如果不能有效处理异常的话,checked exception 就在 API 和代码之间造成了一直负担。程序员就开始写一些空的 catch 代码段,或者仅仅抛出异常,实际上,给客户端的触发者来说增加了负担。

  Checked exception 也被诟病破坏了封装性。看看下面的代码:

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

  getAllAccounts ()抛出了两个 checked exception。这个方法的调用者就必须处理这两个异常,尽管它也不知道在 getAllAccounts 中什么文件找不到以及什么数据库语句失败,也不知道该提供什么文件系统或者数据库的事务层逻辑。这样,异常处理就在方法调用者和方法之间形成了一个不恰当的紧耦合。

  设计 API 的最佳实践

  说了这么多,让我们来说说如何设计一个好的 API,能够正确抛出异常的。

  1. 当要确定是使用 checked exception 还是 unchecked exception 时,首先问问自己,当异常发生时客户端如何应对?

  如果客户端可以从异常中采取行动进行恢复的,就使用 checked exception,如果客户什么也做不了,就用 unchecked exception。我指的是,不仅仅是记录异常,还要采取措施来恢复。

  还有,我更喜欢 unchecked exception,因为不需要强迫客户端 API 必须处理它们。它们会进一步扩散,直到你想 catch 它们,或者它们会继续扩散爆出。Java API 有许多 unchecked exception 如 NullPointerException, IllegalArgumentException 和 IllegalStateException。我更愿意用这些 Java 定义好的异常类,而非我们自己创建的异常类。它们使我们的代码易读,也避免代码消耗更多内存。

  2. 保持封装性

  不要将针对某特定实现的 checked exception 用到更高的层次中去。例如,不要让 SQLException 扩散到逻辑层去。因为逻辑层是不需要知道 SQLException。你有两种选择:

  - 如果你的客户端有应对措施的话,将 SQLException 转化成另一个 checked exception。

  - 如果你的客户端什么也做不了的话,将 SQLException 转化成一个 unchecked exception。

  但大部分情况是,客户端对 SQLException 无能为力。那请将 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 语句需要知道异常发生的根源,我可以用 getCause ()方法,这个方法在 JDK1.4 中所有异常类中都有。

  如果你确信逻辑层可以采取某些恢复措施来应对 SQLException 时,你可以将它转换成更有意义的 checked exception。但我发现仅仅抛出 RuntimeException,大部分时间里都管用。

  3. 如果自定义的异常没有提供有用的信息的话,请不要创建它们。

  下面的代码有什么错误?

public class DuplicateUsernameException
    extends Exception {}

  它没有给出任何有效的信息,除了提供一个异常名字意外。不要忘了 Java 异常类就像其他的类一样,当你在其中增加方法时,你也可以调用这些方法来获得更多信息。

  我们可以在 DuplicateUsernameException 中增加有效的方法,例如:

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

  新版本的 DuplicateUsernameException 提供两个方法:requestedUsername ()返回请求的姓名,availableNames ()返回与请求姓名相类似的所有姓名的一个数组。客户端可以知道被请求的姓名已经不可用了,以及其他可用的姓名。如果你不想获得其他的信息,仅仅抛出一个标准的异常即可:

throw new Exception ("Username already taken");

  如果你认为客户端不会采取任何措施,仅仅只是写日志说明用户名已存在的话,抛出一个 unchecked exception:

throw new RuntimeException ("Username already taken");

  另外,你甚至可以写一个判断用户名是否已经存在的方法。

  还是要重复一遍,当客户端的 API 可以根据异常的信息采取有效措施的话,我们可以使用 checked exception。但对于所有的编程错误,我更倾向于 unchecked exception。它们让你的代码可读性更高。

  4. 将异常文档化

  你可以采用 Javadoc’s @throws 标签将你的 API 抛出的 checked 和 unchecked exception 都文档化。然而,我更喜欢写单元测试。单元测试可看作可执行的文档。无论你选择哪一种方式,都要让客户端使用你的 API 时清楚知道你的 API 抛出哪些异常。下面是针对 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”)会显式的让测试失败。通过写单元测试,你不仅记录了异常如何运作,也让你的代码变得更健壮。

  使用异常的最佳实践

  下面的部分我们列出了客户端代码处理 API 抛出异常的一些最佳实现方法。

  1. 记得释放资源

  如果你正在用数据库或网络连接的资源,要记得释放它们。如果你使用的 API 仅仅使用 unchecked exception,你应该用完后释放它们,使用 try-final。

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 是一个关闭连接的工具类。最重要的部分在于 finally,无论异常发不发生都会执行。在这个例子中,finally 关闭了连接,如果关闭过程中有问题发生的话,会抛出一个 RuntimeException。

  2. 不要使用异常作控制流程之用

  生成栈回溯是非常昂贵的,栈回溯的价值是在于调试。在流程控制中,栈回溯是应该避免的,因为客户端仅仅想知道如何继续。

  下面的代码,一个自定义的异常 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()使用了一个无限的循环来递增计数器,直至异常被抛出。这样写不仅降低了代码的可读性,也让代码变得很慢。记住异常仅用在有异常发生的情况。

  3. 不要忽略异常

  当一个 API 方法抛出 checked exception 时,它是要试图告诉你你需要采取某些行动处理它。如果它对你来说没什么意义,不要犹豫,直接转换成 unchecked exception 抛出,千万不要仅仅用空的{}catch 它,然后当没事发生一样忽略它。

  4. 不要 catch 最高层次的 exception

  Unchecked exception 是继承自 RuntimeException 类的,而 RuntimeException 继承自 Exception。如果 catch Exception 的话,你也会 catch RuntimeException。

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

  上面的代码会忽略掉 unchecked exception。

  5. 仅记录 exception 一次

  对同一个错误的栈回溯(stack trace)记录多次的话,会让程序员搞不清楚错误的原始来源。所以仅仅记录一次就够了。

  总结

  这里是我总结出的一些异常处理最佳实施方法。我并不想引起关于 checked exception 和 unchecked exception 的激烈争论。你可以根据你的需要来设计代码。我相信,随着时间的推移,我们会找到些更好的异常处理的方法的。

  原文:   编译:伯乐在线 – 唐小娟

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多