论坛首页 Java企业应用论坛

为什么 Java 中要使用 Checked Exceptions

浏览 192375 次
该帖已经被评为精华帖
作者 正文
   发表时间:2003-12-17  
关于 Java 中引入的 Checked Exceptions,目前存在着很多反对意见。正方的观点是引入 Checked Exceptions,可以增加程度的鲁棒性。反方的观点是 Checked Exceptions 很少被开发人员正确使用过,并且降低了程序开发的生产率和代码的执行效率。
正方代表 James Gosling
http://www.artima.com/intv/solid.html
反方代表 Anders Hejlsberg
http://www.artima.com/intv/csdes.html
中文版:http://www.csdn.net/develop/article/22/22612.shtm
两位都是大师级的人物,观点的碰撞非常精彩。

我个人更倾向于 James Gosling 的观点,即稳定性更加重要。C++ 程序的后期维护成本远远高于 Java 程序,更不要说前期的开发成本了。Java 要担负企业级的核心应用(HA 24X7),没有这些系统级的保证是不可能的。C++、Delphi 都中没有 Checked Exceptions,所以 Anders 设计 C# 时也没有加 Checked Exceptions。当然我并不是说没有 Checked Exceptions 就一定不能写出稳定的程序(你说,我就是比你牛,我用汇编语言都能写出稳定的程序!),但是显然使用 Java 语言写出稳定的程序比用 C++ 更容易。Checked Exceptions 就是编程语言所提供的系统级保证,能否用好是你自己的问题了,并不象 Anders 说的那样严重。我觉得 Anders 大师对于企业级应用并没有太多的概念。James Gosling 看了 C# 的设计后觉得不过如此。当然这只是大师本人的观点,我们未必一定要接受了。
   发表时间:2003-12-17  
我没有资格评论大师们的观点,但是我知道绝大多数的Java程序员根本就没有领悟“Exception”的真正用处。他们就是把Exception当做异常来理解,没有明白Exception实际上代表了一个UseCase中的异常流的处理。

在使用UseCase来描述一个场景的时候,有一个主事件流和n个异常流。异常流可能发生在主事件流的过程,而try语句里面实现的是主事件流,而catch里面实现的是异常流,在这里Exception不代表程序出现了异常或者错误,Exception只是面向对象化的业务逻辑控制方法。如果没有明白这一点,那么我认为并没有真正明白应该怎么使用Java来正确的编程。

而我自己写的程序,会自定义大量的Exception类,所有这些Exception类都不意味着程序出现了异常或者错误,只是代表非主事件流的发生的,用来进行那些分支流程的流程控制的。例如你往权限系统中增加一个用户,应该定义1个异常类,UserExistedException,抛出这个异常不代表你插入动作失败,只说明你碰到一个分支流程,留待后面的catch中来处理这个分支流程。传统的程序员会写一个if else来处理,而一个合格的OOP程序员应该有意识的使用try catch 方式来区分主事件流和n个分支流程的处理,通过try catch,而不是if else来从代码上把不同的事件流隔离开来进行分别的代码撰写。

总之 Exception <> 异常

BTW:我是支持BE和JG的观点的。我以前在接触C#的时候,就很奇怪C#的这一点了,Anders有他的理由,但是我认为一个良好的面向对象的软件是应该强制使用Exception的。
1 请登录后投票
   发表时间:2003-12-17  
robbin 的话加深了我对于 Exception 的理解。我觉得 try...catch...finally 对于我是一个很好的约束,帮助我编写出更高质量的代码。好象我走路的拐杖一样,我在 Eclipse 中写程序也比在 UltraEdit 中写程序效率高得多,因为 Eclipse 对于我写的代码有更高的要求,UltraEdit 几乎没有任何要求。我很少直接使用 throws Exceptions 把所有 Exceptions 都甩出去的。你不知道程序员有多懒,如果不强制要求,我敢打赌他们 90% 的场合下是完全不用 Exception。
0 请登录后投票
   发表时间:2003-12-17  
另外纠正一个错误的观点:很多人喜欢定义方法的返回类型为boolean型的,当方法正确执行,没有出错的时候返回true,而方法出现出现了问题,返回false。这在Java编程当中是大错而特错的!

方法的返回值只意味着当你的方法调用要返回业务逻辑的处理结果的。如果业务逻辑不带处理结果,那么就是void的,不要使用返回值boolean来代表方法是否正确执行。

例如 用户登陆方法

boolean login(String username, String password);;


很多人喜欢用boolean返回,如果是true,就是login了,如果false就是没有登陆上。其实是错误的。还有的人定义返回值为int型的,例如如果正确返回就是0,如果用户找不到就是-1,如果密码不对,就是-2

int login(String username, String password);;



然后在主程序里面写一个if else来判断不同的流程。

 int logon = UserManager.login(xx,xx);;
 if (logon ==0); {
...
} else if (logon == 1); {
...
} else if (logon ==2); {
..}


这是面向过程的编程逻辑,不是面向对象的编程逻辑。

应该这样来写:

User login(String username, String password); throws UserNotFoundException, PasswordNotMatchException;


主程序这样来写:

try {
  UserManager.login(xx,xx);;
.... 
用户登陆以后的主事件流代码

} catch (UserNotFoundException e); {

...

用户名称没有的事件处理,例如产生一个提示用户注册的页面

} catch (PasswordNotMatchException e); {

....

密码不对的事件处理,例如forward到重新登陆的页面
}
1 请登录后投票
   发表时间:2003-12-17  
如果按照robbin的观点,Exception实际上参与的程序的流程控制。这是不是异常发明者的初衷?这个先不说。有点我不太明白,请教一下。

就用用户登陆的例子,那么我得代码要写成这样:
try{
...
}catch(UserexistException e){
}catch(PasswordisEmptyException e){
}catch(UserPasswordNotMatchException e){
}...

这样写与
if(){
}else if(){
}else if(){
}
有什么太多不同吗?而且效率较低。
主流之流的界定是另外一个问题,比如用户注册,一般来说,用户想注册的id一般来说都是被别人先注册了的。那么catch UserexistException的机会将会多于try中的主流的流程,这是一个极端的情况,我只是想说,主流和之流的界定是很模糊。

关于异常的用法,我很疑惑,平时写代码多半是走走形式。不过我对异常下意识的理解就是运行时,不可预知的意外。如果真的写成:
if(db.findUser(id)){
throw UserExistException("user exist");
}
反而觉得别扭。
0 请登录后投票
   发表时间:2003-12-17  
robbin,并不是我明知顾问,是我们同时发的帖子。转一篇帖子。
我需要一点时间从思想上来接受你的观点。
===========================
Exception 处理之最佳实践



原文:http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html

作者:Gunjan Doshi 2003-11-19

译者注:本文算是一篇学习笔记,仅供学习参考使用,有不妥之处,还请指出。2003-12-04



       “本文是Exception处理的一篇不错的文章,从Java Exception的概念介绍起,依次讲解了Exception的类型(Checked/Unchecked),Exception处理的最佳实现:

1.  选择Checked还是Unchecked的几个经典依据

2.  Exception的封装问题

3.  如无必要不要创建自己得Exception

4.  不要用Exception来作流程控制

5.  不要轻易的忽略捕获的Exception

6.  不要简单地捕获顶层的Exception”

——选自JAVADigest.Net对原文的介绍



   “JAVADigest.Net这个站点不知道大家是否经常上,就像它的名字一样,它让我们更加有效的消化Java,或者它就像个中转站一样,至少对我是这样的,有些好的可以说是非常经典的技术文章,我都是通过它第一次获得,更多的时候我是为了偷懒才上JAVADigest.Net,因为如果是近期比较经典的文章,它上边都有介绍文字和原文连接。”

                                           ——小插曲并非常荣幸地推荐JAVADigest.Net给你







       关于异常处理的一个问题就是要对何时(when)和如何(how)使用它们做到了然于心。在本文中我将介绍一些关于异常处理的最佳实践,同时我也会涉及到最近争论十分激烈的checked Exception的使用问题。

作为开发员,我们都希望能写出解决问题并且是高质量的代码。不幸的是,一些副作用(side effects)伴随着异常在我们的代码中慢慢滋生。无庸置疑,没有人喜欢副作用(side effects),所以我们很快就用我们自己的方式来避免它,我曾经看到一些聪明的程序员用下面的方式来处理异常:



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




上边的代码有什么问题么?



在回答以前让我们想想怎样才是正确的?是的,一旦程序碰到异常,它就该挂起程序而“做”点什么。那么上边的代码是这样子的么?看吧,它隐瞒了什么?它把所有的“苦水”往肚里咽(在控制台打印出异常信息),然后一切继续,从表面上看就像什么都没有发生过一样……,很显然,上边代码达到的效果并不是我们所期望的。



后来又怎样?



public void someMethod() throws Exception{
}




上边的代码又有什么问题?



很明显,上边的方法体是空的,它不实现任何的功能(没有一句代码),试问一个空方法体能抛出什么异常?当然Java并不阻止你这么干。最近,我也遇到类似的情景,方法声明会抛出异常,但是代码中并没有任何“机会”来“展示”异常。当我问开发员为什么要这样做的时候,他回答我说“我知道,它确实有点那个,但我以前就是这么干的并且它确实能为我工作。”



在C++社区曾经花了数年实践来实践如何使用异常,关于此类的争论在java社区才刚刚开始。我曾经看到许多Java程序员针对使用异常的问题进行争论。如果对于异常处理不当的话,异常可以大大减慢应用程序的执行速度,因为它将消耗内存和CPU来创建、抛出并捕获异常。如果过分的依赖异常处理,代码对易读和易使用这两方面产生影响,以至于会让我们写出上边两处“糟糕”代码。



异常原理



大体上说,有三种不同的“情景”会导致异常的抛出:

l         编程错误导致异常(Exception due Programming errors): 这种情景下,异常往往处于编程错误(如:NullPointerException 或者 IllegalArgumentException),这时异常一旦抛出,客户端将变得无能为力。

l         客户端代码错误导致异常(Exception due client code errors): 说白点就是客户端试图调用API不允许的操作。

l         资源失败导致异常(Exception due to resource failures): 如内存不足或网络连接失败导致出现异常等。这些异常的出现客户端可以采取相应的措施来恢复应用程序的继续运行。



Java中异常的类型

Java 中定义了两类异常:

l         Checked exception: 这类异常都是Exception的子类

l         Unchecked exception: 这类异常都是RuntimeException的子类,虽然RuntimeException同样也是Exception的子类,但是它们是特殊的,它们不能通过client code来试图解决,所以称为Unchecked exception

举个例子,下图为NullPointerException的继承关系:


Figure 1. Sample exception hierarchy



图中,NullPointerException继承自RuntimeException,所以它是Unchecked exception.



以往我都是应用checked exception多于Unchecked exception,最近,在java社区激起了一场关于checked exception和使用它们的价值的争论。这场争论起源于JAVA是第一个拥有Checked exception的主流OO语言这样一个事实,而C++和C#都是根本没有Checked exception,它们所有的异常都是unchecked。



一个checked exception强迫它的客户端可以抛出并捕获它,一旦客户端不能有效地处理这些被抛出的异常就会给程序的执行带来不期望的负担。



Checked exception还可能带来封装泄漏,看下面的代码:



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




上边的方法抛出两个异常。客户端必须显示的对这两种异常进行捕获和处理即使是在完全不知道这种异常到底是因为文件还是数据库操作引起的情况下。因此,此时的异常处理将导致一种方法和调用之间不合适的耦合。



接下来我会给出几种设计异常的最佳实践 (Best Practises for Designing the API)



1.  当要决定是采用checked exception还是Unchecked exception的时候,你要问自己一个问题,“如果这种异常一旦抛出,客户端会做怎样的补救?”

[原文:When deciding on checked exceptions vs. unchecked exceptions, ask yourself, "What action can the client code take when the exception occurs?"]

如果客户端可以通过其他的方法恢复异常,那么这种异常就是checked exception;如果客户端对出现的这种异常无能为力,那么这种异常就是Unchecked exception;从使用上讲,当异常出现的时候要做一些试图恢复它的动作而不要仅仅的打印它的信息,总来的来说,看下表:

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不用使客户端代码显示的处理它们,它们自己会在出现的地方挂起程序并打印出异常信息。Java API中提供了丰富的unchecked excetpion,譬如:NullPointerException , IllegalArgumentException 和 IllegalStateException等,因此我一般使用这些标准的异常类而不愿亲自创建新的异常类,这样使我的代码易于理解并避免的过多的消耗内存。



2. 保护封装性(Preserve encapsulation)

不要让你要抛出的checked exception升级到较高的层次。例如,不要让SQLException延伸到业务层。业务层并不需要(不关心?)SQLException。你有两种方法来解决这种问题:

l         转变SQLException为另外一个checked exception,如果客户端并不需要恢复这种异常的话;

l         转变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被抛出,那么程序将抛出RuntimeException,此时程序被挂起并返回客户端异常信息。



如果你有足够的信心恢复它当SQLException被抛出的时候,那么你也可以把它转换为一个有意义的checked exception, 但是我发现在大多时候抛出RuntimeException已经足够用了。



3. 不要创建没有意义的异常(Try not to create new custom exceptions if they do not have useful information for client code.)

看看下面的代码有什么问题?

public class DuplicateUsernameException
    extends Exception {}


它除了有一个“意义明确”的名字以外没有任何有用的信息了。不要忘记Exception跟其他的Java类一样,客户端可以调用其中的方法来得到更多的信息。

我们可以为其添加一些必要的方法,如下:

public class DuplicateUsernameException
    extends Exception {
    public DuplicateUsernameException
        (String username){....}
    public String requestedUsername(){...}
    public String[] availableNames(){...}
}



在新的代码中有两个有用的方法:reqeuestedUsername(),客户但可以通过它得到请求的名称;availableNames(),客户端可以通过它得到一组有用的usernames。这样客户端在得到其返回的信息来明确自己的操作失败的原因。但是如果你不想添加更多的信息,那么你可以抛出一个标准的Exception:

throw new Exception("Username already taken");



更甚的情况,如果你认为客户端并不想用过多的操作而仅仅想看到异常信息,你可以抛出一个unchecked exception:

throw new RuntimeException("Username already taken");



另外,你可以提供一个方法来验证该username是否被占用。



很有必要再重申一下,checked exception应该让客户端从中得到丰富的信息。要想让你的代码更加易读,请倾向于用unchecked excetpion来处理程序中的错误(Prefer unchecked exceptions for all programmatic errors)。



4. Document exceptions.

你可以通过Javadoc’s @throws 标签来说明(document)你的API中要抛出checked exception或者unchecked exception。然而,我更倾向于使用来单元测试来说明(document)异常。不管你采用哪中方式,你要让客户端代码知道你的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")显示说明该测试失败。通过书写测试异常的单元测试,你不但可以看到异常是怎样的工作的,而且你可以让你的代码变得越来越健壮。





下面作者将介绍界中使用异常的最佳实践(Best Practices for Using Exceptions)
1.  总是要做一些清理工作(Always clean up after yourself)

如果你使用一些资源例如数据库连接或者网络连接,请记住要做一些清理工作(如关闭数据库连接或者网络连接),如果你的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.



2. 不要使用异常来控制流程(Never use exceptions for flow control)

下边代码中,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直到抛出异常,这种做法并没有说让代码不易读,但是它是程序执行效率降低。

记住,只在要会抛出异常的地方进行异常处理。



3. 不要忽略异常

当有异常被抛出的时候,如果你不想恢复它,那么你要毫不犹豫的将其转换为unchecked exception,而不是用一个空的catch块或者什么也不做来忽略它,以至于从表面来看象是什么也没有发生一样。



4. 不要捕获顶层的Exception

unchecked exception都是RuntimeException的子类,RuntimeException又继承Exception,因此,如果单纯的捕获Exception,那么你同样也捕获了RuntimeException,如下代码:

try{
..
}catch(Exception ex){
}


一旦你写出了上边的代码(注意catch块是空的),它将忽略所有的异常,包括unchecked exception.



5. Log exceptions just once

    Logging the same exception stack trace more than once can confuse the programmer examining the stack trace about the original source of exception. So just log it once.



总结

这里给出了一些关于异常处理的一些最佳实践,我并不想开始另一轮的关于checked exception 和 unchecked exception的争论。你可以根据自己的实际情况定制自己异常处理,我坚信我们将有更好的办法来处理我们代码中的异常。



在此,我将感谢Bruce Eckel, Joshua Kerievsky, 和Somik Raha对于写这篇文章所给于我的支持。

参考资源:

Related Resources
"Does Java need Checked Exceptions?" by Bruce Eckel
"Exceptional Java," by Alan Griffiths
"The trouble with checked exceptions: A conversation with Anders Hejlsberg, Part II" on Artima.com
"Checked exceptions are of dubious value," on C2.com
Conversation with James Gosling by Bill Venners
关于作者:

Gunjan Doshi works with agile methodologies and its practices and is a Sun certified Java programmer.



2003年12月4日星期四 Jplateau 译于精博
1 请登录后投票
   发表时间:2003-12-17  
2. 不要使用异常来控制流程(Never use exceptions for flow control) 

下边代码中,MaximumCountReachedException被用于控制流程: 

public void useExceptionsForFlowControl(); { 
try { 
while (true); { 
increaseCount();; 
} 
} catch (MaximumCountReachedException ex); { 
} 
//Continue execution 
} 

public void increaseCount(); 
throws MaximumCountReachedException { 
if (count >= 5000); 
throw new MaximumCountReachedException();; 
} 


这和
try {
userLogin();
}
} catch (PasswordNotMatch ex) {
}
//Continue execution
}
void uerLogin(){
if(check(uername,pasword))
throw PasswordNotMatch();
}
是不是有些类似?

我赞同使用try..catch来控制显得更加OO,除此之外还有别的好处吗?
为此付出的代价是增加一些check Exception定义代码,和少许效率。
0 请登录后投票
   发表时间:2003-12-17  
我想应该分两个问题来讨论:
1。什么时候抛出异常--涉及到服务类
2。抛出checked还是unchecked的异常--涉及到客户类

对第一个问题来说,我想异常本身这个字解释了某些东西,异常就是我们认为在正常情况下不可能发生的问题,并且服务代码不知道如何去处理。譬如说我做一个监控程序,需要用压缩卡提供的API去初始化所有的板卡,API提供的是boolean型的返回值,但我把这个API变成抛出一个异常,因为除非特殊原因,我不认为会发生初始化失败的情况,当然更不知道怎样去处理这个问题。又譬如Hibernate里面的LoadObject使用没有发现这个对象存在,那Hibernate也是认为不可能的,除非其他代码直接删除了数据库里面的记录,那么也需要抛出异常。当然Hibernate本身也不知道如何处理这种情况。
但是如果发生的情况是可以预期的,那我不认为应该抛出例外。象上面这个userExist的情况,我认为应该在前面已经分流,应该首先判断这个用户是否存在,if(userExists()),然后进行处理,而不应当抛出例外。以及login应当返回true或者false。也就是说,这些属于程序的正常流程,而不是例外,不是异常。把例外作为正常程序流程的控制机制,只不过是把服务代码中的if转移到客户代码去,没有减少任何需要处理的代码,反而增加了系统的负担(生成例外栈)。
还有抛出异常的情况是违反方法的先决条件,每一个方法都有自己的先决条件和后置条件,方法只有在正确的前提下才能执行达到一个正确的后果,(所谓类的不变量)。譬如你去存取一个数组的某一个元素,这个存取方法有一个前提条件,就是你的索引应当落入它的最大下标和最小下标之间,不然就应当抛出一个例外。

对于第二个问题,端视于客户代码是否能够根据这个例外进行合理的处理。如果客户代码根本就不知道如何处理这个例外,应当把它作为一个unchecked例外,例如上面下标的问题,客户代码用一个不合法的下标来存取数组,那么抛出一个checked例外以后,客户代码是+1还是-1?显然根本就不可能做出“合理的”处理,客户既然不能处理,还要强制它去处理,那么就是捕获,打印了事,没有增加任何价值。但是如果是客户可以处理的,或者可以选择不同的方式处理的,那么就可能需要用checked,但我发现很少有这样的情况。对于类似于RemoteException或者SQLException这些Exception,我一般都转换为具体的业务Exception,而我所有的业务Exception都是RuntimeException.

所以我的观点是,是否抛出例外就是服务代码是否进行合理的处理,抛出什么类型的例外就是客户代码是否能够合理的处理。

当然,Exception应当是有层次的,我的Snip上面有一些说明。
0 请登录后投票
   发表时间:2003-12-17  
我觉得 potian 说的很对。因为性能以及 Exception 设计目的上的原因,不应该用 Exception 来处理本来有可能发生的流程。Exception 就是用来处理真正的异常的。另外 Exception 用得过多对于性能有很大影响,所以要慎重使用。要把 Exception 设计作为整个 OOAD 设计中重要的一部分来考虑。
所以我们得到的结论不是 Java 中是否应该引入 Checked Exceptions 的问题,而是如何用好 Checked Exceptions 的问题。
以前我还买过《Design by Contract 原则与实践》,里面讲的开发方法就更加怪了,可以说整个是一个基于断言的开发方法。好在 JDK 1.4 以后已经提供了 Assert,否则用 Java 是无法实现 DbC 的。目前直接支持这种开发方法的语言好象只有 Eiffel。书中的例子一半用 Eiffel,一半用 Java 的。
0 请登录后投票
   发表时间:2003-12-17  
呵呵,不是三言两语可以说清楚的话题,等有空了我再花时间详细说明我的看法。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics