• 【译】异常处理的最佳实践


    译注:这是一篇2003年的文章,因为时间久远,可能有些观点已经过时,但里面讨论的大部分方法如今仍能适用。

    Best Practices for Exception Handling

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

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

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

    上面的代码有什么错误?

    当异常被抛出后,正常的程序执行过程中断,控制权交给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的继承级别。

     

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

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

    低层次抛出的checked exception,调用层必须要catch或者throw它们。如果客户端代码不能有效处理异常的话,API和代码之间的checked exception很快变成一种负担。客户端代码程序员可能开始写一些空的catch代码块,或者仅仅抛出异常,实际上给客户端的调用者增加了负担。

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

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

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

    设计API的最佳实践

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

    1. 当决定使用checked exception还是unchecked exception时,首先问问自己,"当异常发生时客户端会怎么应对?"

    如果客户端可以从异常中采取行动进行恢复的,就使用checked exception。如果客户端不能采取任何有效措施,就用unchecked exception。有效措施指的是,不仅仅是记录异常日志,还要采取措施来恢复,总结图如下:

    Client's reaction when exception happensException type
    Client code cannot do anything Make it an unchecked exception
    Client code will take some useful recovery action based on information in exception Make it a checked 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 exception和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的激烈争论。你可以根据你的需要来设计代码。我相信,随着时间的推移,我们会找到些更好的异常处理的方法的。

     

    Related Resources

     
  • 相关阅读:
    14. Longest Common Prefix
    7. Reverse Integer
    用例图是软件项目成本预估的好帮手
    设计模式之创建性模式
    代码的核心定义文件
    一个项目经理的经验总结
    设计模式之结构型模式
    互联网发展十几年,你错过了哪些创业机会
    产品经理必读:像怀胎一样怀产品,要厚着脸皮听批评
    陌陌估值1亿美元:一个用户10美元,贵吗?
  • 原文地址:https://www.cnblogs.com/kofxxf/p/3725006.html
Copyright © 2020-2023  润新知