When catching exceptions, don't cast&nbs
摘自JavaWorld,原文请看: Whencatchingexceptions,don'tcastyournbsp;toowide ByDaveSchweisguth 理解Java编译器在编译期是如何检查catch子句的。 摘要 与其它一些语言相比,Java的简单和一致性使编译器能检测许多错误。Java的 开发 者认识到如何依靠编译
摘自
JavaWorld,原文请看:
When catching exceptions, don't cast your .net too wide By Dave Schweisguth
理解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。
- 当重构改进代码时,要检查好那些代码被删除的地方,检查被抛掷的异常被删除的可能性,因为编译器并不总是会告诉你。
如果这些指导方针牢记在心,你就能编写出不同寻常的代码。
全文完
原文转自:http://www.ltesting.net
- 评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)
-
|