这里写下我对 Java 中异常机制的理解,有些知识是来自书本和实践,有些则完全是我的个人观点,对不对的欢迎大家讨论指正 :-)
0 - 异常的处理
=====
异常的处理应该尽量集中在某一层处理,处理异常的地方应该知道足够的上下文信息,知道应该如何处理异常。
对于异常的处理,用户代码(Client Code)掌握上面的简单原则就行了,这不是重点,——重点是异常的定义与抛出(使用)。
1 - 异常前后,各相关部分的状态
某个异常何时发生?
因为什么发生?
一旦异常发生,各相关的对象(数据)都将处于何种状态?(所谓相关对象,就是说方法调用路径所经过的每一层的对象,从 new 异常的那一层一直到 catch 异常的那一层)
或者你无法预料这些对象、数据将会处于什么状态?
程序还能不能继续运行?
如果相关对象、数据在异常发生后都处于可以预料的稳定状态并且程序能继续运行,要不要提示信息给用户?
提示什么信息给用户?
异常有没有可能是用户操作错误导致的?
用户有没有办法通过更正操作等方式调用同一个功能,成功避免这个异常?
如果异常不是用户操作导致的,那么程序继续运行,用户做同样的操作,会不会总是发生这个异常?
=====
-> 如果异常发生后,你不能预料相关对象或数据的状态,或者你预料相关的对象或数据将会处于一种不稳定(或非法)的状态,比如对象内部的状态不再遵从其应有的约束(constraints),如果对对象进行进一步的操作,将会导致不可预料的行为………… 那么这种异常不可恢复,属于应该导致程序崩溃退出的异常。这时,显示友好的信息给用户,比如 “为了防止(对数据产生)更多的损害,程序将停止运行………………”,然后尽最大努力安全结束程序。
这样的异常,应该设计成非检查异常(unchecked exception),一般即 extends RuntimeException 或其子类型。
-> 如果异常是因为非法的用户输入导致的,那么应该在程序中避免,即非法的用户输入应该在其导致异常之前就检验出其不合法,然后不去调用功能,而是反馈合适的信息给用户。
如果检查输入的合法性代价非常昂贵,或者在调用功能之前无法检查其合法性:一种可能是这个功能的设计有问题;如果没有问题,就是无法在调用功能之前检查,或者检查太昂贵,那么就只能 try - catch,这时要明确一旦异常发生,整个调用路径的每一层都将出于什么状态,如果要确保异常发生之后程序能继续运行,那么每一层都应该处于原本调用之前的状态,就像调用没有发生一样。然后你就可以放心给用户提示信息,然后让用户重试了。
(在调用功能之前无法检查其合法性,这种情况大概多存在于多线程环境下,当某接口对外保证线程安全的时候)
这里的异常,可以设计成检查异常(checked exception),也可以设计成非检查异常。这是设计上的取舍问题,如果调用路径很长而你不愿意污染每一层方法的签名(throws),——那么设计成非检查异常。这里的核心问题是注明异常发生后的状态。
2 - 在一个方法被调用以前,有没有办法事先检查这个调用是不是一定会发生异常?
=====
-> 即有没有办法来检查:这个调用到底是一定会发生异常,还是不一定会发生异常?如果有办法检查,——提供这个检查的方法。这样用户代码就不会被迫使用 try - catch 来做流程控制,而是可以先调用这个检查的方法,再决定要不要调用功能方法本身了。要注意的是,这样的设计,会让接口失去“保证线程安全”的可能性,即这样设计的接口不可能向用户代码保证线程安全,——因为两个方法的调用之间产生了逻辑关系。
曾经看到有人问 “怎样得到一个线程安全的List?”,——不存在 “线程安全的List” 这种东西,因为 size() 方法与 get(index) 方法是否会抛出 IndexOutOfBoundsException 之间存在逻辑关系,接口本身无法为这两个相关的调用提供原子性保护。
能够这样先验检查的异常,应该定义成非检查异常,否则用户代码调用了检查方法之后还得再写 try - catch,那个理论上不应该发生的catch 里面你让用户代码写什么呢?
3 - 小结
现在咱们来看看,什么样的异常可以设计成检查异常。
=====
如果一个异常可以定义成检查异常,它需要满足下面这些条件:
- 异常无法在调用方法之前预料
- 异常发生以后,相关的对象、数据将仍然处于一种稳定的状态,即异常被catch处理之后,程序可以(confidently)继续运行
- 异常如此重要,以至于整个调用路径上所有的方法代码都应该意识到它的存在,并且把 throws 写进它们的方法签名
这样的异常,可以定义成检查异常。注意是可以 :-)
—— You can never be wrong to define an exception unchecked. 任何一个异常,你把它定义成非检查异常总不会错。(但是要注意把异常写进 Java DOC,即方法的协议)
事实上 “检查异常” 存在的必要性本身就是有争论的。
对于一个懂得异常处理的程序员,他们对非检查异常也会小心仔细的去处理和设计;对于一个不懂得异常处理的程序员,提供“检查异常”这种机制只会导致大量的滥用,然后大量的接口签名被污染。
如果你发现自己在代码中频繁的 catch 一个检查异常,然后转换抛出一个非检查异常,那这个检查异常的设计多半有问题。(java.io.IOException是不是中枪了?) |
|