分享

Three Rules for Effective Exception Handling

 燮羽 2011-01-07
Java的异常处理机制提供了一个统一的机制来识别和响应程序错误.一个有效的异常处理方法可以使得你的程序健壮并易于调试.因为异常可以对我们解答下面这些问题提供些帮助,因此它是一个对我们极有用的调试工具.
错误是什么?
什么地方发生错误?
为什么发生错误?
(即:3W what、where、why)
当异常使用恰当时,所抛出异常的类型可以表明什么程序错误发生了,而Stack Trace可以告诉我们什么地方发生了错误.至于为什么发生错误则可以通过看异常信息和Stack Trace来了解.如果你发现你的异常不能回答以上所有问题,那一定是你没有把它们用对.当调试程序时,有三个原则将会帮助你更好的使用异常即:具体化、早throw及晚Catch.。
为了说明异常处理的这三个原则,我们将讨论一个叫作JCheckbook的虚拟个人财务管理系统。Jcheckbook可以用作记录和追踪银行帐户的活动,比如存款、取款及开支票。Jcheckbook最初的版本以桌面程序运行,但未来的计划要求以HTML和C/S applet 做为客户端的实现。
具体化
JAVA定义了异常类的层次结构,从Trowable开始, Error和Exception则继承于它,而RuntimeException则继承了Exception.如图所示:
这四个类的抽象层次都很高,它们对了解到底程序发生了什么帮助极少,当你有权实例化这些类的时候,最好将它们视为基类,而使用更特殊的子类,JAVA提供了相当数量的异常子类,并且你也可以对一些特殊的情况定义自己的异常类。
比如说,JAVA的IO包定义了继承于Exception的IOException类,更特殊一点就是FileNotFoundException,EOFException,,ObjectStreamException和所有的IOException的子类。每个异常类都描述了一种特定的与IO有关的错误,它们分别表示:文件丢失,文件意外结束,或者一个被破坏的序列化对象流。异常类越具体,对回答程序发生了什么错误就越有帮助。
在Catch异常时,捕捉具体的异常类显得特别重要,比如,Jcheckbook遇到FileNotFoundException可能会要求你提供另外一个文件。如果是EOFException,程序可能继续运行,只不过提示一下在异常抛出之前文件是可以被正确地读出来的。如一个ObjectStreamException异常抛出了,程序则可能通知用户所读的文件已经被破坏了,并且要求需要提供一个备份或其它的文件。
 
Translator comments:
这也就是为什么要将异常类分类,以此让每个类特定地代表一种程序错误的原因?同时也是为什么在方法声明中throws某个特定的异常类,而不是如:throw Exception?也是为什么要在Catch中捕获具体的异常对象,而不是形如:try{}catch(Excetion e ){}的原因
 
因为在JAVA中,一个try程序块是可以指定多个Catch块,因此就很容易去捕捉具体的异常,然后针对不同的异常提供相应的处理方案。
File prefsFile = new File(prefsFilename);
try
{
    readPreferences(prefsFile);
}
catch (FileNotFoundException e)
{
    // alert the user that the specified file
    // does not exist
}
catch (EOFException e)
{
    // alert the user that the end of the file
    // was reached
}
catch (ObjectStreamException e)
{
    // alert the user that the file is corrupted
}
catch (IOException e)
{
    // alert the user that some other I/O
    // error occurred
}
Jcheckbook为了对所捕获到的异常给用户提供比较具体和特殊点的错误信息,应用了多个Catch块。比如,如果捕获的是FileNotFoundException,它可能通知用户重新指定一个文件。
额外的编写多个Catch块,在某些情况下,可能是多余的负担,但是在这个例子中,它确实可以使程序以一种更加友好的方式回应程序发生的各种错误。
如果是IOException而不是我们起初所指定的那三个异常被抛出了,那么最后一个Catch块将处理并提供一个一般意义上的错误信息。这样,程序在提供某些具体的错误信息的同时也可以对意想不到的那些与文件有关的异常提供一般意义上的处理。
有时候,开发人员直接捕获Exception,然后显示异常类的名字和Stack Trace信息,但为了具体问题具体处理,请不要这样做。看到屏幕上的java.io.EOFException或者Stack Trace信息可能会使用户迷惑,而不是帮助他。捕获具体的异常从而用英语或其它语言给用户一个与问题相关提示,同时将异常的Stack Trace放到LOG文件中。异常和Stack Trace对开发人员意味着一个有力的调试工具,而对用户却毫无用处。
最后,请注意到Jcheckbook将捕获和处理Exception类型异常推迟到用户界面上,而不是放到readPreferences()函数里。这样在界面上可以以对话框的形式提示的用户相关信息或者使用其它的处理方式。这就是我们待会儿将会讨论的“晚捕获”原则
 
Translator comments:
所谓具体化为:
     定义具体的代表某个特定错误的异常类。
     方法声明特定的异常类。
     方法捕获具体类。
目地:具体问题具体处理。
 
早抛出
     通过Stack trace向我们展示的引起异常的方法调用顺序、类名、方法名、原代码文件名以及每个方法调用的行号,这样可以帮助我们精确地定位异常发生的地方。考虑下面的Stack trace信息:
java.lang.NullPointerException
    at java.io.FileInputStream.open(Native Method)
    at java.io.FileInputStream.<init>(FileInputStream.java:103)
    at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:225)
    at jcheckbook.JCheckbook.startup(JCheckbook.java:116)
    at jcheckbook.JCheckbook.<init>(JCheckbook.java:27)
at jcheckbook.JCheckbook.main(JCheckbook.java:318)
这表明类FileInputStream的open方法抛出一个NullPointerException异常,但注意到FileInputStream.open()是JAVA标准类库的一部分。这样引起异常的原因很可能在我们自己代码里面,而不JAVA API,所以问题一定出在这之前的某个方法内,幸运的是它被显示了出来。
很不幸,异常NullPointerException恰好是JAVA中能提供有效信息最少的异常类中的一个。它并不能告诉我们真正想知道的。同时我们也需要向后追踪去找到错误发生地。
     通过向后追踪Stack trace和检查我们的代码,我们发现错误是由于调用方法readPreferences()时传入的文件名为null,因为方法知道一个空文件名将使方法不能再执行下去,所以它立即检查这个条件:
public void readPreferences(String filename)
    throws IllegalArgumentException
{
    if (filename == null)
    {
        throw new IllegalArgumentException
                            ("filename is null");
    } //if
   
    //...perform other operations...
   
    InputStream in = new FileInputStream(filename);
   
    //...read the preferences file...
}
因为比较早的抛出了异常,所以异常就变得更加具体和准确了。Stack trace也很准确地反映出发生了什么异常,为什么及在什么地方。这样使得Stack trace更加准确的反映了本来程序所发生的一切:
java.lang.IllegalArgumentException: filename is null
    at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:207)
    at jcheckbook.JCheckbook.startup(JCheckbook.java:116)
    at jcheckbook.JCheckbook.<init>(JCheckbook.java:27)
at jcheckbook.JCheckbook.main(JCheckbook.java:318)
另外,异常信息(“filename is null”)指出了是什么为空,从而使得异常附带更多有用的信息。而这些是我们无法从异常NullPointerException中获得的。
一旦出错误就立即抛出异常,这样可以避免再去构造或找开那些不再需要的对象或资源。比如说文件或网络连接。与打开这些资源相关的清理工作也可以避免了。
 
晚捕获
     许多JAVA开发人员,无论新手或老手都普遍地犯一个错误就是在程序有能力处理一个异常之前就将它捕获了。JAVA编译器坚持让Checked Exception不是要被捕获就一定要在函数头中声明的作法也客观上促使了程序员采用上面这一错误作法。对程序员来说,一个很自然的趋势就是让代码包括在try程序块中并捕捉异常以阻止编译器报错。
问题是当捕获到异常之后你该如何处理它了?最坏的事情就是不做任何处理。一个空的catch块使异常无端地消失了从而使得关于异常的What、Where、Why信息永远丢失了。将异常的信息Log下来使情况稍好点,毕竟那样还有关于异常信息的记录。但是我们不可能期望用户想看甚至看懂Log文件和Stack trace信息。在函数readPreferences()内显示异常信息对话框是不合适的。因为Jcheckbook目前运行为桌面程序,我们也计划将其改写为基于HTML或者C/S的版本。
功能从Server上获取,而错误需要在浏览器或客户端中显示。我们应该带着为未来着想的想法去设计readPreferences()方法。恰当的将用户交互代码从程序逻辑中分开可以增加代码的可重用性。
在有能力处理一个异常之前去捕获它,常常会进一步地引起其它的错误和异常。比如,像readPreferences()方法立即捕获并处理了在调用FileInputStream构造函数所产生的FileNotFoundException异常,代码如下
public void readPreferences(String filename)
{
    //...
   
    InputStream in = null;
   
    // DO NOT DO THIS!!!
    try
    {
        in = new FileInputStream(filename);
    }
    catch (FileNotFoundException e)
    {
        logger.log(e);
    }
   
    in.read(...);
   
    //...
}
这段代码在对恢复错误无能为力的情况下捕捉了异常FileNotFoundException。如果文件没有找到,剩下的方法体肯定执行不起来。用一个不存在的文件名去调用方法readPreferences()将会发生什么?当然,异常FileNotFoundException将会被Log,如果我们恰好去检查Log文件,我们将会意识到异常的发生。但如果程序继续去读那个文件当中的数据,那会发生什么了?因为文件不存在,in是null,所以一个NullPointerException将会被抛出
当是调试程序时,直觉会使我们在日志文件中去检查最近的信息,你会非常恐惧的看到一个NullPointerException异常,因为它太一般了,不能提供你有价值的对判断异常发生原因有帮助的任何信息,Stack trace也是,这不仅使你无法了解错误是什么(真的错误是FileNotFoundException而不是NullPointerException),而且也无法了解错误发生的正确地点。
 
除了catch异常外,readPreferences()方法还能对异常做何处理了?可能跟我们的直觉相反,仅仅将它上抛就可以了,不要立即catch异常。将处理的责任上抛给readPreferences()的调用者,让它去决定采用合适的方法去处理文件引用丢失的问题,它可以提示用户要求另一个文件、使用默认的值或者如果没有其它的办法,提示用户有问题发生并退出程序的运行也是可以。
这种将异常处理的任务上抛给它的调动链上的方法就是用throws关键字在方法头声明这些需要上抛的异常。当声明这些异常时,请尽可能使用能代表具体问题的异常类。这样可以使调动你方法的程序可以预料到到底会发生哪些异常并根椐具体情况处理它们。将前面代码修改如下:
public void readPreferences(String filename)
    throws IllegalArgumentException,
           FileNotFoundException, IOException
{
    if (filename == null)
    {
        throw new IllegalArgumentException
                        ("filename is null");
    } //if
   
    //...
   
    InputStream in = new FileInputStream(filename);
   
    //...
}
从技术上说,我们唯一需要声明的就是IOException,但是通过声明此方法可能会抛出FileNotFoundException可以帮助调用者更好的处理异常。而异常IllegalArgumentException是不需要声明的,因为它是一个Unchecked Exception(为RuntimeException的子类)仍然将其包括在其中是因为这样使代码更清晰[特别对调用者而言]。
当然,最终你的程序仍需捕获异常,要么程序就会意外地中止。但这个原则的意义就在于,它可以让你在合适地方捕获异常并能够提供合适的异常处理方法,不会再引起其它的异常,从而让程序能继续运行。或者提供用户一些具体有用的信息,包括如何从错误中恢复过来一些指示。当一个方法对异常不能做这两者之一时,简单地将异常上抛,这样它就会在合适的地方被捕获并处理。
总结
有经验的开发人员都知道调试程序最难的部分不是修改Bug,而是找出Bug的藏身之处。通过使用上面所讲的三个原则,你就可以使异常帮助你追踪和清除Bugs并且使得你的程序更加健壮和友好。
Leon·He
Broaden Gate(ShenZhen) Corp
回归简单,严戒浮躁.
 
附原文:
 

Exceptions in Java provide a consistent mechanism for identifying and responding to error conditions. Effective exception handling will make your programs more robust and easier to debug. Exceptions are a tremendous debugging aid because they help answer these three questions:

  • What went wrong?
  • Where did it go wrong?
  • Why did it go wrong?

When exceptions are used effectively, what is answered by the type of exception thrown, where is answered by the exception stack trace, and why is answered by the exception message. If you find your exceptions aren't answering all three questions, chances are they aren't being used effectively. Three rules will help you make the best use of exceptions when debugging your programs. These rules are: be specific, throw early, and catch late.

To illustrate these rules of effective exception handling, this article discusses a fictional personal finance manager called JCheckbook. JCheckbook can be used to record and track bank account activity, such as deposits, withdrawals, and checks written. The initial version of JCheckbook runs as a desktop application, but future plans call for an HTML client and a client/server applet implementation.

Be Specific

Java defines an exception class hierarchy, starting with Throwable, which is extended by Error and Exception, which is then extended by RuntimeException. These are illustrated in Figure 1.


Figure 1. Java exception hierarchy

These four classes are generic and they don't provide much information about what went wrong. While it is legal to instantiate any of these classes (e.g., new Throwable()), it is best to think of them as abstract base classes, and work with more specific subclasses. Java provides a substantial number of exception subclasses, and you may define your own exception classes for additional specificity.

For example, the java.io package defines the IOException subclass, which extends Exception. Even more specific are FileNotFoundException, EOFException, andObjectStreamException, all subclasses of IOException. Each one describes a particular type of I/O-related failure: a missing file, an unexpected end-of-file, or a corrupted serialized object stream, respectively. The more specific the exception, the better our program answers what went wrong.

It is important to be specific when catching exceptions, as well. For example, JCheckbook may respond to a FileNotFoundException by asking the user for a different file name. In the case of an EOFException, it may be able to continue with just the information it was able to read before the exception was thrown. If an ObjectStreamException is thrown, the program may need to inform the user that the file has been corrupted, and that a backup or a different file needs to be used.

Java makes it fairly easy to be specific when catching exceptions because we can specify multiple catch blocks for a single try block, each handling a different type of exception in an appropriate manner.

File prefsFile = new File(prefsFilename);
try
{
readPreferences(prefsFile);
}
catch (FileNotFoundException e)
{
// alert the user that the specified file
// does not exist
}
catch (EOFException e)
{
// alert the user that the end of the file
// was reached
}
catch (ObjectStreamException e)
{
// alert the user that the file is corrupted
}
catch (IOException e)
{
// alert the user that some other I/O
// error occurred
}

JCheckbook uses multiple catch blocks in order to provide the user with specific information about the type of exception that was caught. For instance, if a FileNotFoundException was caught, it can instruct the user to specify a different file. The extra coding effort of multiple catch blocks may be an unnecessary burden in some cases, but in this example, it does help the program respond in a more user-friendly manner.

Should an IOException other than those specified by the first three catch blocks be thrown, the last catch block will handle it by presenting the user with a somewhat more generic error message. This way, the program can provide specific information when possible, but still handle the general case should an unanticipated file-related exception "slip by."

Sometimes, developers will catch a generic Exception and then display the exception class name or stack trace, in order to "be specific." Don't do this. Seeing java.io.EOFException or a stack trace printed to the screen is likely to confuse, rather than help, the user. Catch specific exceptions and provide the user with specific information in English (or some other human language). Do, however, include the exception stack trace in your log file. Exceptions and stack traces are meant as an aid to the developer, not to the end user.

Finally, notice that instead of catching the exception in the readPreferences() method, JCheckbook defers catching and handling the exception until it reaches the user interface level, where it can alert the user with a dialog box or in some other fashion. This is what is meant by "catch late," as will be discussed later in this article.

Throw Early

The exception stack trace helps pinpoint where an exception occurred by showing us the exact sequence of method calls that lead to the exception, along with the class name, method name, source code filename, and line number for each of these method calls. Consider the stack trace below:

java.lang.NullPointerException
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.<init>(FileInputStream.java:103)
at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:225)
at jcheckbook.JCheckbook.startup(JCheckbook.java:116)
at jcheckbook.JCheckbook.<init>(JCheckbook.java:27)
at jcheckbook.JCheckbook.main(JCheckbook.java:318)

This shows that the open() method of the FileInputStream class threw a NullPointerException. But notice that FileInputStream.close() is part of the standard Java class library. It is much more likely that the problem that is causing the exception to be thrown is within our own code, rather than the Java API. So the problem must have occurred in one of the preceding methods, which fortunately are also displayed in the stack trace.

What is not so fortunate is that NullPointerException is one of the least informative (and most frequently encountered and frustrating) exceptions in Java. It doesn't tell us what we really want to know: exactly what is null. Also, we have to backtrack a few steps to find out where the error originated.

By stepping backwards through the stack trace and investigating our code, we determine that the error was caused by passing a null filename parameter to the readPreferences() method. Since readPreferences() knows it cannot proceed with a null filename, it checks for this condition immediately:

public void readPreferences(String filename)
throws IllegalArgumentException
{
if (filename == null)
{
throw new IllegalArgumentException
("filename is null");
}  //if
//...perform other operations...
InputStream in = new FileInputStream(filename);
//...read the preferences file...
}

By throwing an exception early (also known as "failing fast"), the exception becomes both more specific and more accurate. The stack trace immediately shows what went wrong (an illegal argument value was supplied), why this is an error (null is not allowed for filename), and where the error occurred (early in the readPreferences() method). This keeps our stack trace honest:

java.lang.IllegalArgumentException: filename is null
at jcheckbook.JCheckbook.readPreferences(JCheckbook.java:207)
at jcheckbook.JCheckbook.startup(JCheckbook.java:116)
at jcheckbook.JCheckbook.<init>(JCheckbook.java:27)
at jcheckbook.JCheckbook.main(JCheckbook.java:318)

In addition, the inclusion of an exception message ("filename is null") makes the exception more informative by answering specifically what was null, an answer we don't get from theNullPointerException thrown by the earlier version of our code.

Failing fast by throwing exceptions as soon as an error is detected can eliminate the need to construct objects or open resources, such as files or network connections, that won't be needed. The clean-up effort associated with opening these resources is also eliminated.

Catch Late

A common mistake of many Java developers, both new and experienced, is to catch an exception before the program can handle it in an appropriate manner. The Java compiler reinforces this behavior by insisting that checked exceptions either be caught or declared. The natural tendency is to immediately wrap the code in a try block and catch the exception to stop the compile from reporting errors.

The question is, what to do with an exception after it is caught? The absolute worst thing to do is nothing. An empty catch block swallows the exception, and all information about what, where, and why something went wrong is lost forever. Logging the exception is slightly better, since there is at least a record of the exception. But we can hardly expect that the user will read or even understand the log file and stack trace. It is not appropriate for readPreferences() to display a dialog with an error message, because while JCheckbook currently runs as a desktop application, we also plan to make it an HTML-based web application. In that case, displaying an error dialog is not an option. Also, in both the HTML and the client/server versions, the preferences would be read on the server, but the error needs to be displayed in the web browser or on the client. The readPreferences() method should be designed with these future needs in mind. Proper separation of user interface code from program logic increases the reusability of our code.

Catching an exception too early, before it can properly be handled, often leads to further errors and exceptions. For example, had the readPreferences() method shown earlier immediately caught and logged the FileNotFoundException that could be thrown while calling the FileInputStream constructor, the code would look something like this:

public void readPreferences(String filename)
{
//...
InputStream in = null;
// DO NOT DO THIS!!!
try
{
in = new FileInputStream(filename);
}
catch (FileNotFoundException e)
{
logger.log(e);
}
in.read(...);
//...
}

This code catches FileNotFoundException, when it really cannot do anything to recover from the error. If the file is not found, the rest of the method certainly cannot read from the file. What would happen should readPreferences() be called with the name of file that doesn't exist? Sure, the FileNotFoundException would be logged, and if we happened to be looking at the log file at the time, we'd be aware of this. But what happens when the program tries to read data from the file? Since the file doesn't exist, in is null, and a NullPointerException gets thrown.

When debugging a program, instinct tells us to look at the latest information in the log. That's going to be the NullPointerException, dreaded because it is so unspecific. The stack trace lies, not only about what went wrong (the real error is a FileNotFoundException, not a NullPointerException), but also about where the error originated. The problem occurred several lines of code away from where the NullPointerException was thrown, and it could have easily been several method calls and classes removed. We end up wasting time chasing red herrings that distract our attention from the true source of the error. It is not until we scroll back in the log file that we see what actually caused the program to malfunction.

What should readPreferences() do instead of catching the exceptions? It may seem counterintuitive, but often the best approach is to simply let it go; don't catch the exception immediately. Leave that responsibility up to the code that calls readPreferences(). Let that code determine the appropriate way to handle a missing preferences file, which could mean prompting the user for another file, using default values, or, if no other approach works, alerting the user of the problem and exiting the application.

The way to pass responsibility for handling exceptions further up the call chain is to declare the exception in the throws clause of the method. When declaring which exceptions may be thrown, remember to be as specific as possible. This serves to document what types of exceptions a program calling your method should anticipate and be ready to handle. For example, the "catch late" version of the readPreferences() method would look like this:

public void readPreferences(String filename)
throws IllegalArgumentException,
FileNotFoundException, IOException
{
if (filename == null)
{
throw new IllegalArgumentException
("filename is null");
}  //if
//...
InputStream in = new FileInputStream(filename);
//...
}

Technically, the only exception we need to declare is IOException, but we document our code by declaring that the method may specifically throw a FileNotFoundException.IllegalArgumentException need not be declared, because it is an unchecked exception (a subclass of RuntimeException). Still, including it serves to document our code (the exceptions should also be noted in the JavaDocs for the method).

Of course, eventually, your program needs to catch exceptions, or it may terminate unexpectedly. But the trick is to catch exceptions at the proper layer, where your program can either meaningfully recover from the exception and continue without causing further errors, or provide the user with specific information, including instructions on how to recover from the error. When it is not practical for a method to do either of these, simply let the exception go so it can be caught later on and handled at the appropriate level.

Conclusion

Experienced developers know that the hardest part of debugging usually is not fixing the bug, but finding where in the volumes of code the bug hides. By following the three rules in this article, you can help exceptions help you track down and eradicate bugs and make your programs more robust and user-friendly.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多