专栏名称: 狗厂
目录
相关文章推荐
51好读  ›  专栏  ›  狗厂

猫头鹰的深夜翻译:JAVA中异常处理的最佳实践

狗厂  · 掘金  ·  · 2018-03-01 03:27

正文

前言

异常处理的问题之一是知道何时以及如何去使用它。我会讨论一些异常处理的最佳实践,也会总结最近在异常处理上的一些争论。

作为程序员,我们想要写高质量的能够解决问题的代码。但是,异常经常是伴随着代码产生的副作用。没有人喜欢副作用,因此我们会试图用自己的方式来解决这个问题。我看过不少的程序用下面的方法应对异常:

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

上面这段代码的问题在哪里?

一旦一个异常被抛出之后,正常的执行流程会停止并且将控制交给捕捉块。捕捉块捕获异常,然后只是把它的信息打印了一下。之后程序正常运行,就像没有任何事情发生一样。

那下面的这种方法呢?

public void someMethod() throws Exception{
}

这是一个空方法,里面没有任何的代码。为什么一个空方法能够抛出异常?JAVA并不阻止你这么做。最近,我遇到了一些和这个很相似的代码,明明代码块中没有抛出异常的语句,却在方法声明中抛出异常。当我问开发人员为什么这么做,他会回答“我知道这样会影响API,但是我之前就这么做的而且效果还不错”。

C++社区花了好久才决定如何使用异常。这场争论也在JAVA社区产生了。我看到不少JAVA开发人员艰难的使用异常。如果不能够正确使用的话,异常会影响程序的性能,因为它需要使用内存和CPU来创建,抛出以及捕获。如果过度使用的话,会使得代码难以阅读,并且影响API的使用人员。我们都知道这将会带来代码漏洞以及坏味道。客户端代码常会通过忽略这个异常或是直接将其抛出来避开这个问题,就像之前的两个例子那样。

异常的本质

从广义的角度来说,一共有三种不同的场景会导致异常的产生:

  • 编程错误导致的异常:这一类的异常是因为不恰当的编程带来的(比如 NullPointerException , IllegalArgumentException )。客户端通常无法对这些错误采取任何措施
  • 客户端代码的错误:客户端代码在API允许的范围之外使用API,从而违背了合约。客户端可以通过异常中提供的有用信息,采用一些替代方法。比如,当解析格式不正确的XML文件时,会抛出异常。这个异常中包含导致该错误发生的XML内容的具体位置。客户端可以通过这些信息采取回复措施。
  • 资源失效导致的异常:比如系统内存不足或是网络连接失败。客户端面对资源失效的回应是要根据上下文来决定的。客户端可以在一段时间之后试着重新连接或是记录资源失效日志然后暂停应用程序。

JAVA异常类型

JAVA定义了两种异常:

  • 需检查的异常:从 Exception 类继承的异常都是需检查异常。客户端需要处理API抛出的这一类异常,通过try-catch或是继续抛出。
  • 无需检查的异常: RuntimeException 也是 Exception 的子类。但是,继承了 RuntimeException 的类受到了特殊的待遇。客户端代码无需专门处理这一类异常。

下图展示了 NullPointerException 的继承树:

上图中, NullPointerException 继承自 RuntimeException ,因此它也是一个无需检查的异常。

我看到过大量使用需检查异常只在极少数时候使用无需检查异常的。最近,JAVA社区在需检查异常的真正价值上爆发了热烈的讨论。这场辩论源于JAVA是第一个包含需检查异常的主流OO框架。C++和C#根本没有需检查异常。这些语言中所有的异常都是无需检查的。

从低层抛出的需检查异常强制要求调用方捕获或是抛出该异常。如果客户端不能有效的处理该异常,API和客户端之间的异常协议将会带来极大的负担。客户端的开发人员可能会通过将异常抑制在一个空的捕获块中或是直接抛出它。从而又将这个负担交给了客户端的调用方。

还有人指责需检查异常会破坏封装,看下面这段代码:

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

getAllAccounts() 方法抛出了两个需检查异常。调用这个方法的客户端必须明确的处理这两种具体的异常,即使它们并不清楚 getAllAccount() 内究竟是哪个文件访问或是数据库访问失败了,而且它们也没有提供文件系统或是数据库的逻辑。因此,这样的异常处理导致方法和调用者之前出现了不当的强耦合。

设计API的最佳实践

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

1.在选择抛出需确定异常或是无需确定异常时,问自己这样的一个问题:客户端代码在遇到异常时会进行怎样的处理?
如果客户端能够采取措施从这个异常中恢复过来,那就选择需确定异常。如果客户端不能采取有效的措施,就选择无需确定异常。有效的措施是指从异常中恢复的措施,而不仅仅是记录错误日志。

除此以外,尽量选择无需确定的异常:它的优点在于不会强迫客户端显式地处理这种异常。它会冒泡到任何你想捕获它的地方。JAVA API提供了许多无需检查的异常如 NullPointerException , IllegalArgumentException IllegalStateException 。我倾向于使用JAVA提供的标准的异常,尽量不去创建自己的异常。

2.保留封装
永远不要将特定于实现的异常传递到更高层。比如,不要将数据层的 SQLException 传递出去。业务层不需要了解 SQLException 。你有两个选择:

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

大多数时候,客户代码无法解决 SQLException 。这时候就将其转化为无需检查的异常。







请到「今天看啥」查看全文