When catching exceptions, don't cast your net too wide

来源:互联网 发布:淘宝店铺二次审核2016 编辑:程序博客网 时间:2024/04/28 00:43
When catching exceptions, don't cast your net too wide
kevin 翻译  (参与分:282,专家分:1130)   发表:2003-10-27 下午10:11   更新:2003-12-2 下午4:24   版本:1.1   阅读:2255

摘自JavaWorld,原文请看:When catching exceptions, don't cast your net too wide 
By Dave Schweisguth 

http://www.javaresearch.org/article/showarticle.jsp?column=331&thread=9876
理解Java编译器在编译期是如何检查catch子句的。
摘要
与其它一些语言相比,Java的简单和一致性使编译器能检测许多错误。Java的开发者认识到如何依靠编译器对不正确的类型、不存在的方法的调用(这篇文章的主题)和不正确的异常处理进行捕获。但是在你真正需要知道你正在做什么的地方,那些你不想看到的情形仍会突然出现。如果你能确切的理解Java是怎样让你抛掷和捕获异常的,那么你就会知道你什么时候需要特别小心,什么样的习惯能让你远离烦恼。

Java的编译期检查对保持异常安全的框架进行了完美的支持,如果一个方法声明会抛掷一个异常,你无法在你的方法中不用捕获这个异常或声明你的方法也会抛掷这个异常的情况下调用那个方法。(更广泛的讨论请看“Designing with Exception”)编译器有时也会阻止你去捕获一个在try块中没有抛掷的异常,但并不总是如此,大多数时候都不会。这篇Java Tip又一次讨论了编译期检查。

throw子句的编译期检查


首先,让我们区别一下 Java 如何检查catch子句捕获的异常 与  Java如何检查一个方法中声明的会被抛掷出的异常。(在这篇文章中,当我用小写字母e开头来说exception时,那就指java.lang.Throwable和它的子类。当我想要指明一个明确的类时,像java.lang.Exception,我会包含包句或至少以大写字母开头的类名。)刚开始,这种方法看起来似乎很类似:两者都通过代码块的关联指明了预期被抛出的异常。但是,当Java要求一个方法声明它抛出的异常时,它并不是有所依托的要求那个方法抛出每一个声明的异常,Java允许你设计出在你添加了功能时程序保持稳定的API。
看一下下面这个自造的连接池的原始版本:

public class ConnectionPool {
   public ConnectionPool() throws ConnectionException {
   }

   public Connection getConnection() throws ConnectionException {
      // Allocate a connection (possibly throwing a ConnectionException or a
      // subclass) if necessary, then return it
   }
}

构造函数什么也不做,而在getConnection()方法中的代码可能会抛出一个ConnectionException的异常,所以在这种实现中,方法实际上不需要声明任何异常。但是在下一个版本中,我们重写了这个类以提高getConnection()的速度:

public class ConnectionPool {
   public ConnectionPool() throws ConnectionException {
      // Allocate all the connections we think we'll ever need
   }

   public Connection getConnection() throws ConnectionException {
      // Allocate a connection if necessary (not likely), then return it
   }
}

因为我们在第一个版本中写的构造函数声明了ConnectionException,所以使用它的代码不用为使用第二个版本而改写。Java对throw子句进行了一些检查,以便在所有其它调用这个构造函数的类中都有始终如一的稳定性--这是一种很好的方式。

catch子句的编译期检查


catch子句与throw子句是有不同的内容。API 稳定性的观点在这不适用:当一个方法声明是一个类的公共接口的一部分的时候,一个try/catch块就是一个从调用者的角度隐藏细节的实现。不但没有理解因为一个catch子句而去捕获一个try块没有抛掷的异常,而且保证它不会那么做以便能捕捉严重的代码错误。因为这个原因,Java要求你的try块确实抛出了一个它们的catch子句捕获的异常。例如,假设你对念念不忘以前的操作系统,并且写了下面一小段代码:

public class rm {
   public static void main(String[] args) {
      for (int i = 0; i < args.length; i++) {
         try {
            new File(args[i]).delete();
         } catch (IOException e) {     // Won't compile!!!
            System.err.println("rm: Couldn't delete " + args[i]);
         }
      }
   }
}


Sun微系统公司的Java编译器将会告诉你IOException "is never thrown in body of corresponding try statement." 那是因为File.delete()根本没有抛出任何异常,而是在不能删除文件时返回false。如果不是编译期catch子句的异常检查,你可能会无意中写了一个失败的却没有明显错误的程序。
“但是,等等”,你是一个有思想且经验丰富的编码员,“人们一直在庞大的代码中捕获java.lang.Exception,并且在那些try块中并不总是抛出java.lang.Exception,但是仍然能编译通过!”你是对的。这里我们很快就达到我们的目标:究竟在对一个catch子句中允许捕获的异常进行检查时Java使用的规则是什么?

catch子句也会捕获子类


答案有两部分:第一是,一个catch子句捕获它的参数类型的异常和它的子类。这也是一个有价值的语言特性:一个声明抛出异常的方法,比如异常javax.naming.NamingException,那么这个方法确实能抛出许多任何NamingException的子类。方法的调用者需要知道为一个特殊子类能写一个catch子句;这样就不能仅仅捕获NamingException。更进一步,如果一个会抛出NamingException子类的方法的实现在后来的版本中改变了,那么原始的实现不需要改变,它的调用者也不需要改变。这种灵活性也赋予了API更大的稳定性。
事实上,catch子句捕获子类也会有一些小麻烦。例如,许多读者可能写过类似下面这样的实用方法:

public class ConnectionUtil {
   /** Close the connection silently. Keep going even if there's a problem. */
   public static void close(Connection connection) {
      try {
         connection.close();
      } catch (Exception e) {
         // Log the message (using the JDK 1.4 logger) and move on
         Logger.global.log(Level.WARNING, "Couldn't close connection", e);
      }
   }
}

对这个程序来说在关闭一个连接时的错误可能无法显现,所以我们刚好能捕获和记录它们。但是,记住RuntimeException继承自Exception。如果我们捕获Exception ,正如在这个例子中所示,我们也会捕获RuntimeException。一个空连接传递到这个方法可能是一种比在关闭一个有效连接失败时更加严重的情形--可能意味着编程错误。如果试图调用close(),就会抛出NullPoingerException,你想要这个错误传播到椎栈并到达你的严重错误管理者,而不是一个警告错误。有解决方法吗?有,不要捕获Exception,而是捕获由你正在调用的方法所抛掷的特殊的异常。如果它抛出多个异常,那么分别捕获每一个,或一个公共的超类(只要它不是Exception)

对于那些公用的超类也要小心。捕获Exception是最普遍的情形,编译期检查会让你在这儿通过,捕获任何子类的异常都有类似的情形。你可能想要分别处理java.io.FileNotFoundException和java.io.EOFException,那就不要只捕获java.io.IOException使它们无法区别。一般而言,如果你的代码以相同方式处理某个异常的子类,那你就可以只捕获这个超类。

错误和运行期异常(RuntimeException)都可被捕获而不论它们是否被抛出


单独来说,catch子句能捕获子类的事实并没有解释为什么下面的代码片段可以编译通过,即使唯一的可能抛出异常的代码行也被注释掉了:

try {
   //doSomething();
   // Commented out during development
catch (Exception e) {
   Logger.global.log(Level.SEVERE, "Something failed", e);
}


另一个让人困惑的地方是那两个没有显式使用的异常,java.lang.Error和java.lang.RuntimeException,它们完全避免了编译期的检查。catch子句可以捕获这种异常无论它们实际上是否被抛掷。Java语言规范(Java Language Specification)(JLS)解释了为什么这些异常可以在不用捕获或声明的情况下被抛掷:主要原因是来自JVM 的错误(Error)可能在任何地点发生,而来自众多语言结构的运行期异常(RuntimeException)也可能会在任何地点发生。对它们进行检查将不只会增加编译器的编写难度,而且也会强迫程序员去处理那些本来他们什么也不用做的情形。
现在,我们继续讨论catch子句的检查。JLS并没有明确指出Error和RuntimeException在无论是否声明被抛出的情况下可以被捕获,但是这却是Sun 的编译器的行为(一个空的try块或是只有一条不会抛出任何异常的语句的try块可以有一个能捕获Error或RuntimeException的catch子句)。


放到一块来观察


到现在我们仍然没有解释为什么Exception可以被捕获,即使它没有明确的被抛掷。放到一块来看:RuntimeException继承自Exception,所以如果RuntimeException能被在任何地点抛掷,那Exception也能在任何地点被抛掷。与此类似,Error继承自Throwable,所以尽管用的是Throwable,Error照样能被捕获无论它是否被明确的抛出。
Java这种异常检查规则的逻辑导致了一种尴尬的结果。每个为了赶任务的人都会随手拿java.lang.Exception来用。你可以想象一下,交工的最后期限到了,而你正在为一个新的工具包而努力,可是它却有难以计数的错误条件,并且你没有时间去分别处理它们。即使工具包的所有异常都继承自一些公用子类,你也不想马上找出它们,这时为了让它能运行,你封装那些代码在一个捕获Exception的try块中。哈:你正好为你自己或为将要编辑你的代码的人设计了一个陷阱。随后,当你重构你的原型代码时,你可能会分割那个庞大的try块。那将导致在那个块中抛出的异常不再被抛出。事实上,正如在上面的例子所示,即使你以根本不会抛出任何异常的代码结尾--但是因为你将要捕获Exception,所以你无法从编译器中找出它。那意味着周围是毫无价值的try/catch块,臃肿混乱的代码。所以如果可以就不要捕获Exception,如果非要那样做,那记住稍后要回来修复那些catch子句。

有时,捕获Exception也是有意义的。比如在你应用程序的最终错误处理器或在一个应用程序服务器的执行引擎中,你不想让一个服务中的一处代码错误而关闭整个服务器。但是大多数代码不需要这种特殊优特。
注意:IBM的Java编译器--Jikes--在你没有明确声明会抛出Throwable 或Exception而捕获了它们时,不会警告你,像许多其它条款在Sun的编译器中没有实现一样。然而,在你只抛出了一个异常的子类而你却捕获了它的超类时,它也不会警告。对于Jikes我没有足够经验来建议你如何例行使用,但是你可以很容易的试一下,如果你感兴趣,我还是建议你检验一下。


最佳习惯


最后概括一下:Java允许你在捕获异常时遗失错误处理信息:你可以捕获一个超类,并且丢失由子类传递的特殊信息。假设你根本没有在你的代码中抛掷任何异常,在你捕获Exception或Throwable时,你也不会被告知有任何错误。所以,怎样才能远离麻烦呢?

  • 尽可能的捕获最特殊的异常。只有在你确信你将要捕获的所有异常对你的代码有相同的意义时,才去捕获一个超类,并且这个超类会在某个新类中某个方法的未来版本中会被抛掷。否则如果可以永远都不要捕获Exception或Throwable。

  • 如果非得捕获Exception或Throwable,考虑一下从其它Exception或Throwable的子类中分别处理。记住下面一条金规玉律:你自己永远都不要抛出Exception或Throwable

  • 当重构改进代码时,要检查好那些代码被删除的地方,检查被抛掷的异常被删除的可能性,因为编译器并不总是会告诉你。


如果这些指导方针牢记在心,你就能编写出不同寻常的代码。

==========================原文=============================
Page 1 of 2

Advertisement

Java's compile-time checking does a pretty good job of keeping exceptions safely caged—you can't call a method that throws a checked exception without catching the exception or declaring that your own method throws that exception. (For a great discussion on checked and unchecked exceptions and when to use each, see "Designing with Exceptions" (JavaWorld, 1998).) The compiler will also sometimes stop you from catching an exception that isn't thrown in the try block, but not always, and not when you need it most. This Java Tip discusses this second compile-time check.

Compile-time checking of throws clauses
First, let's distinguish how Java checks the exceptions a method declares it throws from how it checks the exceptions that a catch clause catches. (In this article, when I say exception with a lowercase e, I mean java.lang.Throwable and its subclasses. When I mean a specific class, like java.lang.Exception, I include the package or at least capitalize the class name.) Initially, the approaches seem quite similar: both indicate the exceptions expected to be thrown by the code block with which they're associated. But while Java requires a method to declare the exceptions that it throws, it doesn't require a method to throw every exception it declares for a good reason: Java allows you to design APIs that remain stable as you add functionality.

Consider this initial version of a home-brewed connection pool:

public class ConnectionPool {
   public ConnectionPool() throws ConnectionException {
   }

   public Connection getConnection() throws ConnectionException {
      // Allocate a connection (possibly throwing a ConnectionException or a
      // subclass) if necessary, then return it
   }
}

While the code in getConnection() might throw a ConnectionException, the constructor does nothing, so in this implementation, the method doesn't really need to declare any exceptions. But in the next version, we might rewrite the class to speed up getConnection():

public class ConnectionPool {
   public ConnectionPool() throws ConnectionException {
      // Allocate all the connections we think we'll ever need
   }

   public Connection getConnection() throws ConnectionException {
      // Allocate a connection if necessary (not likely), then return it
   }
}

Because we made the constructor in the first version declare ConnectionException, code that uses it doesn't have to change to use the second version. Java trades some checking it could do in the throws clause for the sake of long-term stability in all the other classes that call the constructor—not a bad bargain at all.

Compile-time checking of catch clauses
Catch clauses are a different story from throws clauses. The API stability argument doesn't apply: while a method declaration is part of a class's public interface, a try/catch block is an implementation detail hidden from callers. Not only is there no reason for a catch clause to catch an exception that its try block doesn't throw, guaranteeing that it doesn't do so can catch serious coding errors. For these reasons, Java requires your try blocks to actually throw the exceptions that their catch clauses catch. For example, if you were homesick for your old operating system and wrote the following little utility,

public class rm {
   public static void main(String[] args) {
      for (int i = 0; i < args.length; i++) {
         try {
            new File(args[i]).delete();
         } catch (IOException e) {     // Won't compile!!!
            System.err.println("rm: Couldn't delete " + args[i]);
         }
      }
   }
}

Sun Microsystems' Java compiler would tell you that IOException "is never thrown in body of corresponding try statement." That is because File.delete() doesn't throw any exceptions at all. Instead, it returns false if it can't delete the file. If it weren't for this compile-time exception checking of the catch clause, you might have accidentally written a program that fails silently.

"But wait a minute," you seasoned coders are thinking, "people catch java.lang.Exception in sloppy code all the time, and the code in those try blocks doesn't always throw java.lang.Exception, but it still compiles!" You're right, and here we come to the goal of our expedition: what rule does Java really use when checking the exceptions that a catch clause is allowed to catch?

Catch clauses catch subclasses too
The answer has two parts: The first is that a catch clause catches exceptions of the type of its parameter and its subclasses. This is a valuable language feature as well: a method that declares it throws, for example, javax.naming.NamingException, can really throw any of NamingException's many subclasses. That method's callers needing to know about a specific subclass can write a catch clause for it; those that don't can simply catch NamingException. Furthermore, if the method's implementation changes in a later version, it can throw NamingException subclasses, which the original implementation did not, and its callers need not change. This flexibility contributes greatly to API stability as well. (See "Exceptional Practices, Part 1" (JavaWorld, 2001) for more discussion on how to design an exception hierarchy and throws clauses.)

The fact that catch clauses catch subclasses can also get you into a bit of trouble. For example, many readers might have written something like this utility method:

public class ConnectionUtil {
   /** Close the connection silently. Keep going even if there's a problem. */
   public static void close(Connection connection) {
      try {
         connection.close();
      } catch (Exception e) {
         // Log the message (using the JDK 1.4 logger) and move on
         Logger.global.log(Level.WARNING, "Couldn't close connection", e);
      }
   }
}

Errors in closing a connection probably don't mean much to the application, so we just catch and log them. But remember that RuntimeException extends Exception. If we catch Exception, as in the example, we'll catch RuntimeException too. A null Connection passed into this method is possibly a much more serious situation—probably indicating a programming error—than failure to close an otherwise valid Connection. If the attempt to call close() throws NullPointerException, you want the error to propagate up the stack to your serious-error handler, not be mistaken for a warning! The solution? Rather than catching Exception, catch the specific exception thrown by the method you're calling. If it throws more than one exception, catch each individually, or catch a common superclass (as long as it's not Exception).

Be careful with those common superclasses too: while catching Exception is the most common case where compile-time checking will let you down, catching any exception with subclasses can put you into a similar situation. You probably want to handle java.io.FileNotFoundException and java.io.EOFException differently, so don't just catch java.io.IOException and lose the distinction. In general, catch an exception superclass only if it's okay for your code to handle each of its subclasses the same way.


Errors and RuntimeExceptions can be caught whether or not they're thrown
By itself, the fact that catch clauses catch subclasses doesn't explain why the following code fragment compiles, even though the only line of code that might throw an exception has been commented out:

try {
   //doSomething(); // Commented out during development
} catch (Exception e) {
   Logger.global.log(Level.SEVERE, "Something failed", e);
}

The second piece of the puzzle is that two exceptions are unchecked, completely exempt from compile-time checking: java.lang.Error and java.lang.RuntimeException. Catch clauses may catch these exceptions whether or not they're actually thrown. Section 11.2 of the Java Language Specification (JLS) explains why these exceptions may be thrown without being caught or declared: briefly, Errors come from the JVM and may occur anywhere, and RuntimeExceptions come from many language constructs (listed in section 15.6 of the JLS) and may occur almost anywhere. Checking for them would not only prove difficult for compiler writers, but would also force programmers to handle situations they can do nothing about.

Now, back to checking catch clauses: the JLS doesn't say specifically that Error and RuntimeException may be caught whether or not they're actually thrown, but that's how Sun's compiler behaves—an empty try block or one containing only statements that throw no exceptions of any kind may have catch clauses that catch Error or RuntimeException.

Put the pieces together
We still haven't explained why Exception, which is checked, can be caught even when it's apparently not thrown. Put the pieces together: RuntimeException extends Exception, so if RuntimeException can be thrown anywhere, Exception can be thrown anywhere too. Similarly, Error extends Throwable, so although Throwable is checked, it can be caught whether or not it is explicitly thrown.

This logical result of Java's exception-checking rules has an awkward consequence. Everyone first reaches for java.lang.Exception when in a hurry. You know the feeling: you're on deadline, you're struggling with some new toolkit, it has a zillion error conditions, and you just don't have time to deal with them separately. Even if all the toolkit's exceptions extend some common subclass, you're not going to look it up right now. To keep moving, you wrap your code in a try block and catch Exception. Oops: you just laid a trap for yourself or the next person who edits your code. Later, when you refactor your prototype, you'll probably split your big try block. Sometimes, that will result in an exception that was thrown in that try block no longer being thrown. In fact, as in the example above, you might even end up with no code that throws any exceptions at all—but since you're catching Exception, you won't find it out from the compiler. That means useless try/catch blocks hanging around, code bloat, and confusion. So don't catch Exception if you can help it—and if, in a pinch, you do, go back later and tighten up those catch clauses.

Sometimes, catching Exception makes sense, such as in your application's last-ditch error handler or in the execution engine of an application server where you don't want a coding error in one service to bring down the whole server. But most code doesn't need this special treatment. Think twice!

Note that IBM's Java compiler, Jikes, does warn you about catching Throwable or Exception without explicitly throwing them—as well as many other items not caught by Sun's compiler. However, it doesn't warn about catching an exception superclass when only a subclass is thrown. I don't have enough experience with Jikes to recommend using it routinely, but it's easy to try, and I certainly recommend checking it out if you're interested.

Best practices
To sum up, Java allows you to lose error-handling information when you catch an exception: you can catch a superclass instead and lose the specific information conveyed by the subclass. If you catch Exception or Throwable, you won't even be told if your code doesn't throw an exception at all. So, how to stay out of trouble?

  • Catch the most specific exception that you can. Catch a superclass only if you're certain that all the exceptions you'll catch by doing so have the same meaning to your code—and consider what new subclasses future versions of a method might want to throw too. Never catch Exception or Throwable if you can help it.

  • If you're forced to catch Exception or Throwable, consider handling RuntimeException or Error separately from other Exception or Throwable subclasses. And follow the golden rule: never throw Exception or Throwable yourself.

  • As you refactor evolving code, watch for situations where removing code removes possibilities for exceptions to be thrown—the compiler won't always tell you.

With these guidelines in mind, you'll be better prepared to write even more exceptional code.



 


原创粉丝点击