异常处理原则--good

异常机制是现代主流语言的标配,但是异常处理问题虽然已经被讨论很多,也有很多经典书籍的论述,却一直都充满争议。很多人都觉得异常处理很难拿捏,同时也难以理解一些语言或库的异常处理设计。我使用Java近10年,但直到最近我才感觉完全理清了对于异常处理的种种疑惑,下面就介绍一下我对Java异常处理原理和原则的一些认识,欢迎交流探讨!

Exception和Error的区别

谈异常处理的第一个问题是:什么是异常?什么又不是异常?这个问题看似简单,其实很多人都没有分辨清楚,尤其是人们经常混用异常(Exception)、错误(Error)、失败(Failure)、缺陷(Bug)这些接近又有区别的词。

这里最需要对比区分的是Failure/Exception和Bug/Error。用例子来说,你尝试打开一个文件失败了,触发了一个IOException,这是一种运行时遇到的操作失败,它并不代表你的程序本身有问题
但是,Bug就不一样了,假设你有一个sort函数对数组进行排序,如果发现调用sort之后居然还有乱序情况,导致整个系统行为出错最后crash,那么这就不是异常而是错误,唯一的解决办法是修改程序解决Bug
所以,在Java中我们可以这样区分,异常(Exception)是一种非程序原因的操作失败(Failure),而错误(Error)则意味着程序有缺陷(Bug)
注意:其他语言术语可能不同,重要的是能从概念上区分它们。

Java的类继承体系非常清楚地区分了Exception和Error。

java.lang.Object
    java.lang.Throwable
        java.lang.Error
        java.lang.Exception

Exception下面是我们常见的各种异常类,Error下面最著名的就是AssertionError,它可以通过throw new AssertionError(...)显式抛出,也可以通过assert操作符产生。Java文档中明确说到:

An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch.

就是说一般情况下不应该尝试用catch(Throwable)或者catch(Error)去捕获Error,因为抛出这个Error就是希望整个程序马上停下来。
可能有人会疑惑:“如果不捕获Error,程序crash了后果很严重啊”?这个就要靠自己结合具体情况去判断了,让程序带着已经发作的Bug跑还是立刻停下来,到底哪个后果更严重?有时是前者,有时是后者。

声明异常和未声明异常的区别

Java可以在方法签名上显式地声明可能抛出的异常,但也允许抛出某些未声明的异常。那么,二者有何区别呢?
我们自己在设计一个方法时如何决定是否在方法上声明某个异常呢?本质上讲,在方法签名上声明的异常属于方法接口的一部分,它和方法的返回值处于同一抽象层次,不随具体实现的变化而改变
比如,Integer类用于解析一个字符串到Integer型整数的valueOf方法:

public static Integer valueOf(String s) throws NumberFormatException

它声明抛出的NumberFormatException属于这个方法接口层面的一种失败情况,不管内部实现采用什么解析方法,都必然存在输入字符串不是合法整数这种情况,所以这时把这个异常声明出来就非常合理。相反,下面这个从帐户a向帐户b转账的transfer方法:

public boolean transfer(Account a, Account b, Money money) throws SQLException

它抛出SQLException就不对了,因为SQLException不属于这个transfer接口层面的概念,而属于具体实现,很有可能未来某个实现不用SQL了那么这个异常也就不存在了。
这种情况下,就应该捕获SQLException,然后抛出自定义异常TransferException,其中TransferException可以定义几种和业务相关的典型错误情况,比如金额不足,帐户失效,通信故障,同时它还可以引用SQLException作为触发原因(Cause)。

public boolean transfer(Account a, Account b, Money money) throws TransferException {
    try {
        ...
        executeSQL(...);
    } catch (SQLException e) {
        throw new TransferException("...", e);
    }
}

什么情况下方法应该抛出未声明的异常?

前面谈到在编写一个方法时,声明异常属于接口的一部分,不随着具体实现而改变,但是我们知道Java允许抛出未声明的RuntimeException,那么什么情况下会这样做呢?比如,下面的例子中方法f声明了FException,但是它的实现中可能抛出RuntimeException,这是什么意思呢?

void f() throws FException {
    if (...) {
        throw new RuntimeException("...");
    }
}

根据上面提到的原理,未声明异常是和实现相关的,有可能随着不同实现而出现或消失,同时它又对应不到FException。比如,f方法依赖于对象a,结果在运行时a居然是null,导致本方法无法完成相应功能,这就可以成为一种未声明的RuntimeException了(当然,更常见的是直接调用a的方法,然后触发NullPointerException)。

其实,很多情况下抛出未声明的RuntimeException的语义和Error非常接近,只是没有Error那么强烈,方法的使用者可以根据情况来处理,不是一定要停止整个程序。我们最常见的RuntimeException可能要算NullPointerException了,通常都是程序Bug引起的,如果是C/C++就已经crash了,Java给了你一个选择如何处理的机会。

所以,抛出未声明异常表示遇到了和具体实现相关的运行时错误,它不是在设计时就考虑到的方法接口的一部分,所以又被称为是不可恢复的异常。有些Java程序员为了简便不声明异常而直接抛出RuntimeException的做法从设计上是不可取的。

如何捕获和处理其他方法抛出的异常?

下面例子中方法g声明了GException,方法f声明了FException,而f在调用g的时候不管三七二十一通过catch (Exception e)捕获了所有的异常。

void g() throws GException
void f() throws FException {
    try {
        g();
    } catch (Exception e) {
        ...
    }
    ....
}

这种做法是很多人的习惯性写法,它的问题在哪里呢?问题就在于g明明已经告诉你除了GException外,如果抛出未声明的RuntimeException就表示遇到了错误,很可能是程序有Bug,这时f还不顾一切继续带着Bug跑。所以,除非有特殊理由,对具体情况做了分析判断,一般不捕获未声明异常,让它直接抛出就行。

void g() throws GException
void f() throws FException {
    try {
        g();
    } catch (GException e) {
        ...
    }
    ....
}

但是,很遗憾有一种很典型的情况是g()是不受自己控制的代码,它虽然只声明了抛出GException,实际上在实现的时候抛出了未声明的但属于接口层面而应该声明的异常。如果遇到这种情况最好的做法是应该告诉g()的作者修改程序声明出这些异常,如果实在不行也只能全部捕获了。但是,对于自己写的程序来讲,一定要严格区别声明异常和未声明异常的处理,这样做的目的是理清Exception和Bug的界限

自定义异常应继承Exception还是RuntimeException?

Java中区分Checked Exception和Unchecked Exception,前者继承于Exception,后者继承于RuntimeException。Unchecked Exception和Runtime Exception在Java中常常是指同一个意思。

public boolean createNewFile() throws IOException

上面的IOException就是一个著名的Checked Exception。Java编译器对Checked Exception的约束包括两方面:对于方法编写者来讲,Checked Exception必须在方法签名上声明;对于方法调用者来讲,调用抛出Checked Exception的方法必须用try-catch捕获异常或者继续声明抛出。相反,Unchecked Exception则不需要显式声明,也不强制捕获。

Checked Exception的用意在于明确地提醒调用者去处理它,防止遗漏。但是Checked Exception同时也给调用者带来了负担,通常会导致层层的try-catch,降低代码的可读性,前面例子中的Integer.valueOf方法虽然声明了NumberFormatException,但是它是一个RuntimeException,所以使用者不是必须用try-catch去捕获它

实际上,对于自己编写的异常类来讲,推荐默认的是继承RuntimeException,除非有特殊理由才继承Exception。C#中没有Checked Exception的概念,这种推荐的做法等于是采用了C#的设计理念:把是否捕获和何时捕获这个问题交给使用者决定,不强制使用者。当然,如果某些情况下明确提醒捕获更加重要还是可以采用Checked Exception的。对于编写一个方法来讲,“是否在方法上声明一个异常”这个问题比“是否采用Checked Exception”更加重要。

总结

本文介绍了自己总结的Java异常处理的主要原理和原则,主要回答了这几个主要的问题:
1)Exception和Error的区别;
2)声明异常和未声明异常的区别;
3)什么情况下应抛出未声明异常
4)理解如何捕获和处理其他方法抛出的异常;
5)自定义异常应继承Exception还是RuntimeException。
最后需要说的是,虽然有这些原理和原则可以指导,但是异常处理本质上还是一个需要根据具体情况仔细推敲的问题,这样才能作出最合适的设计。

http://www.cnblogs.com/weidagang2046/p/exception-handling-principles.html

 

时间: 2024-09-24 13:08:24

异常处理原则--good的相关文章

Java程序异常处理的特殊情况

1.不能在finally块中执行return,continue等语句,否则会把异常"吃掉"; 2.在try,catch中如果有return语句,则在执行return之前先执行finally块 请大家仔细看下面的例子: public class TryTest {public static void main(String[] args) {try {System.out.println(TryTest.test());// 返回结果为true其没有任何异常} catch (Except

EJB异常处理的最佳做法

随着 J2EE 成为企业开发平台之选,越来越多基于 J2EE 的应用程序将投入生产.J2EE 平台的重要组件之一是 Enterprise JavaBean(EJB)API.J2EE 和 EJB 技术一起提供了许多优点,但随之而来的还有一些新的挑战.特别是企业系统,其中的任何问题都必须快速得到解决.在本文中,企业 Java 编程老手 Srikanth Shenoy 展现了他在 EJB 异常处理方面的最佳做法,这些做法可以更快解决问题. 在 hello-world 情形中,异常处理非常简单.每当碰到

Java有效处理异常的三个原则_java

异常之所以是一种强大的调试手段,在于其回答了以下三个问题:      1.什么出了错?      2.在哪出的错?      3.为什么出错? 在有效使用异常的情况下,异常类型回答了"什么"被抛出,异常堆栈跟踪回答了"在哪"抛出,异常信息回答了"为什么"会抛出,如果你的异常没有回答以上全部问题,那么可能你没有很好地使用它们. 有三个原则可以帮助你在调试过程中最大限度地使用好异常,这三个原则是:      1.具体明确      2.提早抛出   

浅谈java异常处理(父子异常的处理)_java

我当初学java异常处理的时候,对于父子异常的处理,我记得几句话"子类方法只能抛出父类方法所抛出的异常或者是其子异常,子类构造器必须要抛出父类构造器的异常或者其父异常".那个时候还不知道子类方法为什么要这样子抛出异常,后来通过学习<Thinking in Java>,我才明白其中的道理,现在我再来温习一下. 一.子类方法只能抛出父类方法的异常或者是其子异常 对于这种限制,主要是因为子类在做向上转型的时候,不能正确地捕获异常 package thinkinginjava; p

Java中异常处理方法总结

异常(Exception):指程序运行过程中出现的非正常现象. 1.  Java异常的异常处理机制 早期的情况: 早期使用的程序设计语言是没有提供专门进行异常处理功能的,程序设计人员只能苦逼的使用条件语句对各种可能设想到的错误情况进行判断,来捕捉特定的异常,然后进行相应的处理.这样的处理方式,往往要整出大段大段的if-else语句.本来需要完成相应功能的代码块很小,但是加上这样针对异常处理的条件语句使得代码显得非常臃肿,这样一来代码的可读性和可维护性就下降了,而且有时候还会遗漏意想不到的异常情况

一道关于java异常处理的题目_java

1.建立exception包,编写TestException.java程序,主方法中有以下代码,确定其中可能出现的异常,进行捕获处理. public class YiChang { public static void main(String[] args){ for(int i=0;i<4;i++){ int k; switch(i){ case 0: int zero=0; try{ k=911/zero; }catch(ArithmeticException e){ System.out.

一个用于J2EE应用程序的异常处理框架

在大多数Java项目中,大部分代码都是样板代码.异常处理就属于此类代码.即使业务逻辑只有3到4行代码,用于异常处理的代码也要占10到20行.本文将讨论如何让异常处理保持简单和直观,使开发人员可以专心于开发业务逻辑,而不是把时间浪费在编写异常处理的样板代码上.本文还将说明用于在J2EE环境中创建和处理异常的基础知识和指导原则,并提出了一些可以使用异常解决的业务问题.本文将使用Struts框架作为表示实现,但该方法适用于任何表示实现. 使用checked和unchecked异常的场景 您是否曾经想过

JAVA【异常二】异常处理机制

Java中异常提供了一种识别及响应错误情况的一致性机制,有效地异常处理能使程序更加健壮.易于调试.异常之所以是一种强大的调试手段,在于其回答了以下三个问题: 什么出了错? 在哪出的错? 为什么出错? 在有效使用异常的情况下,异常类型回答了"什么"被抛出,异常堆栈跟踪回答了"在哪"抛出,异常信息回答了"为什么"会抛出.   在Java 应用程序中,异常处理机制为:抛出异常,捕捉异常. 抛出异常:当一个方法出现错误引发异常时,方法创建异常对象并交付运

J2EE开发过程中的异常处理

j2ee|过程|异常处理 在java里有3种异常类型: 1. 检查型异常,这样的异常继承于Excetpion,就是在编译期间需要检查,如果该异常被throw,那么在该异常所在的method后必须显示的throws,调用该method的地方也必须捕获该异常,否则编译器会抛出异常.ejb里的RemoteException是一个这样的异常. 2. 运行时异常,就是在运行期间系统出现的异常,该类异常继承于RuntimeException,该类异常在编译时系统不进行检查,如NullPointerExcet