本文是关于 Exception 处理的一篇不错的文章,从 Java Exception 的概念介绍起,依次讲解了 Exception 的类型(Checked/Unchecked),Exception 处理的最佳实现:html
Best Practices for Exception Handling
By Gunjan Doshi 11/19/2003
原文连接: http://www.onjava.com/pub/a/o...
关于异常处理的问题之一就是要知道什么时候(when)和如何(how)使用它。在本文中我将介绍一些关于异常处理的最佳实践,同时我也会总结最近关于 checked Exception 使用问题的一些争论。java
做为程序员,咱们都但愿能写出解决问题而且是高质量的代码。不幸的是,异常是伴随着咱们的代码产生的反作用(side effects)。没有人喜欢反作用(side effects),因此咱们很快就找到(find)了咱们本身的方式来避免它,我曾经看到一些聪明的程序员用下面的方式来处理异常:react
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),
客户端代码能够经过忽略异常或抛出异常来避开这个问题,如前两个示例所示。网络
从广义上讲,有三种不一样的情景会致使异常的抛出:app
NullPointerException
和IllegalArgumentException
),客户端一般没法对这些编程错误采起任何措施。Java 定义了两类异常:ide
Exception
类继承的异常都是检查型异常(checked exceptions),客户端必须处理API抛出的这类异常,经过catch
子句捕获或是经过throws
子句继续抛出(forwarding it outward)。RuntimeException
也是 Exception
的子类,然而,从RuntimeException
继承的全部异常都会获得特殊处理。客户端代码不须要专门处理这类异常,所以它们被称为 Unchecked exceptions.NullPointerException
的继承关系。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)。
在讨论了这些以后,如今让咱们来探讨一下如何设计一个正确抛出异常的API。
若是客户端能够采起措施从异常中恢复,那就选择 checked exception 。若是客户端不能采起有效的措施,就选择 unchecked exceptions 。有效的措施是指从异常中恢复的措施,而不只仅是记录异常日志。总结一下:
Client's reaction when exception happens | Exception 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 来处理编程错误:unchecked exception 的优势在于不强制客户端显示的处理它,它会传播(propagate)到任何你想捕获它的地方,或者它会在出现的地方挂起程序并报告异常信息。Java API中提供了丰富的 unchecked excetpion,如:NullPointerException
, IllegalArgumentException
和 IllegalStateException
等。我更倾向于使用JAVA提供的标准异常类而不肯建立新的异常类,这样使个人代码易于理解并避免过多的消耗内存。
永远不要让特定于实现的 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
已经足够用了。
如下代码有什么问题?
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)。它们使你的代码更具可读性。
你可使用 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")
显式的说明该测试失败了。经过为异常编写单元测试,你不只能够记录异常如何触发,还可使你的代码在通过这些测试后更加健壮。
下一组最佳实践展现了客户端代码应如何处理抛出 checked exception 的API。
若是你在使用如数据库链接或是网络链接之类的资源,请记住要作一些清理工做 (如关闭数据库链接或者网络链接),若是你调用的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
。
生成堆栈跟踪 (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直到抛出异常,这种方式使得代码难以阅读,并且影响代码性能。只在要会抛出异常的地方进行异常处理。
当API中的方法抛出 checked exception 时,它在提醒你应当采起一些措施。若是 checked exception 没有任何意义,请绝不犹豫的将其转化为 unchecked exception 再从新抛出。而不是用一个空的 catch
块捕捉来忽略它,而后继续执行,以致于从表面来看仿佛什么也没有发生同样。
unchecked exception
都是 RuntimeException
的子类,而 RuntimeException
又继承自 Exception
,若是单纯的捕获 Exception
, 那么你一样也捕获了 RuntimeException
,如如下代码所示:
try{ // ... }catch(Exception ex){ }
上边的代码(注意catch
块是空的)将忽略全部的异常,包括 unchecked exception
.
将相同的异常屡次记入日志会使得检查追踪栈的开发人员感到困惑,不知道何处是报错的根源。因此只记录一次。
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.