原文地址:http://www.mindview.net/Etc/Discussions/CheckedExceptions
仅为了凑篇翻译而试翻译,不当之处请见谅。
Java需要预期异常吗?
尽管C++引进了异常的规范,Java是唯一一个实施这一规范的主流语言,使用了Checked Exception(预期异常)。在以下的讨论中,我将检视其积极性和这个实验的结果,并考虑其他的(可能更有用的)方式去管理异常。此次讨论的目标是探索这些想法――尤其是,你是怎样处理异常的?使用un-checked exception对你而言更有利吗?
我是在异常被引入C++委员会的时候开始了解异常的,这是一条漫长的学习曲线。维护异常最重要的理由是,它们可以让程序员减少错误检查代码的书写,因为这些代码可以被推迟到更适当的时机,而不是不得不在每次调用时都加上测试――没人是这么做的。事实上,我认为C语言匮乏的错误处理模型是C++引入异常处理的动机,因为我们真正需要的是统一一致的错误报告方式(不幸的是,因为C++是向后兼容C语言的,C++里的异常处理仅仅只是一个附加的错误处理模型)。
起初预期异常看起来确实是个好想法。但这都是基于以下我们未被挑战的假设――静态类型检查检测异常,并且总是最好的。Java是第一个使用预期异常的(就我所知),所以就成了第一个实验品。然而,你必须用这些代码包围被测试的部分,被捕获的异常暗示着存在问题。在Python中,异常是未经检查的,你可以捕获他们并做些你想做的,但不是强制的。这看上去也很好。
我觉得这是编译时和运行时检查的问题。我们已经习惯了只在唯一正确的道路上思考,唯一安全、可靠的方式,在编译的时候做这些事情,以至于我们不自觉地怀疑任何依赖于运行时的解决方案。这种思想来源于C++,但我注意到Java实际上在运行时做了不少事,仅仅只是因为它们不能在编译时完成而被接受。但是尽管如此我们始终坚持如果它可以在编译时完成,那么那是最佳的时机(这里也针对Python里的弱类型)。我知道这是一个看起来不够清晰和易于证明的思考方式,但如果你开始有了似乎反驳这种普遍思考方式的经历,然后你就会开始质疑。
我从去年夏天开始这方面的探讨,同时我开始从别人那里听说到――预期异常是个错误。他们不存在于Python、c#或者C++。实际上,我只知道在Java里有,我敢打赌这是因为人们在C++的未经检查的异常的规格中看到,从而认为这是错误的(我知道我是这样的,在很长一段时间里)。在这点上,我感觉预期异常是(1)引入Java时是一个未经试验的试验(除非你知道它在其他语言里被实现过,Ada?有可能)(2)一个失败的经验,因为很多人最终选择在他们的代码中抑制异常。
在Python/C#的方法中,异常被抛出,如果你想要的话可以编写代码去捕获,但不这样做的话,你也没有被强制去写一大堆额外的代码,被怂恿去抑制异常。
我最近打算为了第三版去重写异常这个篇章(和书里的其他部分),改写异常处理的方式。
现在我是这样看待异常的:
1)异常的重大价值是错误报告的统一化:通过一个标准的机制报告错误,好过C里面使用的小方法锦集(C++只是混合加入了异常,而没有提炼出专有的方法)。Java大大超过C++的优势就在于异常是唯一报告错误的方式。
2)前面提到的可以忽略的东西(小方法锦集)是另一个问题。这个理论讲的是如果编译器驱使编程者要么处理异常,要么用一种异常规范传递出去,然后,编程者就总会去关注出错的可能,才能适宜的处理他们。我认为问题在于这是我们提出的一个未经测试的假设,就像语言设计者进入了心理学领域。我认为当一个人想试着做一件事的时候,你总使用无聊的东西打扰他,他们会用身边最便捷的东西去驱赶这些,这样他们才能完成自己的事,也可以假设他们之后会回来把那东西(指之前驱赶无聊之物的东西)拿走。我发现在Thingking In Java的第一版中我已经做过这些:
... } catch (SomeKindOfException e) {}
在重写之前多少忘了一些。有多少人认为这是个好例子并且也这样做的呢?我开始审视这种同类型的代码,并且意识到人们剔除异常,然后异常跟着不见了。预期异常的开销对其的起初的使用目的有了反作用,在你试验的时候可以发现(我现在认为预期异常是一个基于有人认为这是个好想法而做的试验,而我直到最近才认为这是个好想法)。
当我开始用Python的时候,所有的异常都出现,从没有一个忽然不出现的。如果你想捕获异常也可以,但你没有被强制去写扩展代码,只要一直传送出去就可以了。他们会被抛到你想捕获他们的地方,如果你忘了就一直抛出(从而提醒着你),但他们不会消失,这是最糟糕的情况了。我现在确信预期异常鼓励人们去使他们(异常)消失。更好的是他们使可读的代码大大减少。
最后,我认为我们必须认识到预期异常的实验性,假设所有Java中关于异常的东西都是对的之前,仔细的审视他们。我坚信会有一个的很棒的异常处理机制,并且我认为使用一个分离的通道(异常处理手法)把异常包围起来是好的。但是我记得,早期C++中处理异常的观点之一,是允许程序员把想完成功能的代码和处理异常的代码分离开来,但对我而言,预期异常似乎不是做这些的;相反的,它们倾向于侵入到你正常工作的代码中,因此是一种倒退。我对Python中异常的经验也是支持这种,除非我花时间在这个问题上,我更打算在我的Java代码中加入更多的运行时异常(RuntimeExceptions)。
有件事对我来说已经是非常明确,特别是由于Python:你给程序员越多随机的规则,而这些对解决他们手头的问题完全没用,他们完成的就越慢。这不会是一个线性因素,而是指数级的。
我得到一两分报告,关于有人声称预期异常在产品代码中是一个问题(被吞噬)以至于他们想改变这种情况。你可能认为那些大部分程序员是新手。我不止一次在研讨会中看到――有多年编程经验的人不理解一些基础的东西。
可能这只是时间上的问题。当关于异常的想法被引入C++委员会的时候,我就开始研究他们,经过这么多时间,它突然惊醒了我。但我由于使用了有一种也引入异常,但不是预期异常的语言而对异常存在质疑。我认为两者融合是最好的――如果我想捕获异常,就可以捕获,但我不必去吞噬它,只要避免写大堆的代码。如果我不写东西包围异常,忽略它们,并且在调试期间有出现了就会报告给我,然后我可以决定怎么处理它们。我仍然处理异常,但我一直没有被强制去写关于异常的大堆代码。
ExceptionAdapter,这是我在Heinz Kabutz帮助下开发时使用的一个工具。它把任何预期异常都转换成运行时异常,同时保留预期异常的所有信息。
import java.io.*; class ExceptionAdapter extends RuntimeException { private final String stackTrace; public Exception originalException; public ExceptionAdapter(Exception e) { super(e.toString()); originalException = e; StringWriter sw = new StringWriter(); e.printStackTrace(new PrintWriter(sw)); stackTrace = sw.toString(); } public void printStackTrace() { printStackTrace(System.err); } public void printStackTrace(java.io.PrintStream s) { synchronized(s) { s.print(getClass().getName() + ": "); s.print(stackTrace); } } public void printStackTrace(java.io.PrintWriter s) { synchronized(s) { s.print(getClass().getName() + ": "); s.print(stackTrace); } } public void rethrow() { throw originalException; } }
原始异常被存储在originalException这个字段里,所以你在任意时候都可以提取出来。此外,它的堆栈跟踪信息被提取到stackTrace这个字符串中,如果异常被输入到控制台,可以到时使用普通的printStackTrace()方法打印出来。然而,你还可以在你的程序中的更高层加入catch语句去捕获一个ExceptionAdapter,去查看它的特有类型,比如这样:
catch(ExceptionAdapter ea) { try { ea.rethrow(); } catch(IllegalArgumentException e) { // ... } catch(FileNotFoundException e) { // ... } // etc. }
在这,你仍可以捕获特定类型的异常,但你没被强制在每个异常发生的地方和它被捕获的地方之间使用异常规范,加入try-catch语句。更最要的是,没有哪个人写代码是去吞噬异常和消除它。如果你忘记去捕获一些异常,它会在最高层被显示出来。如果你想在地方中间捕获异常,你就可以这么做。
或者,既然originalException这个字段是public的,你可以通过RTTI(Run-Time Type Information,运行时类型信息)寻找特定类型的异常。
下面是一些测试代码,只为了确定它可以运行(但不是我建议的使用方式):
public class ExceptionAdapterTest { public static void main(String[] args) { try { try { throw new java.io.FileNotFoundException("Bla"); } catch(Exception ex) { ex.printStackTrace(); throw new ExceptionAdapter(ex); } } catch(RuntimeException e) { e.printStackTrace(); } System.out.println("That's all!"); } }
通过使用这个工具,你可以使用未预期异常的途径中获益(更少、更简洁的代码),但又不会丢失关于异常的核心信息。
如果你在写代码的时候想抛出特定类型的预期异常,你可以像下面这样使用(或者如果还不行,修改一下)ExceptionAdapter:
if(futzedUp) throw new ExceptionAdapter(new CloneNotSupportedException());
这意味着你可以轻松的用异常的原始角色来使用所有的异常,但是用的是非预期异常风格的编码。
Kevlin Henney 写道:
我必须承认,基于Java和C++的使用经验,阅读其他语言的编码,我得出了相似的结论。关于异常,我唯一的声明是,我发现,CORBA风格的异常规格是有用的,错误通过重要的界限,比如说机床边界被传递,这些接口需要更精确。然而,尽管表面上和Java的异常机制相似,但并没有编译时检查的概念。
在你的文章澄清一些观点,Ada没有任何形式的异常规格,所以也没有一个预期/未预期的模型。异常规格的根源,要追溯到CLU(Command Line Utility,命令行使用程序),或者更久之前。关于这个的关键论文是《Exception Handling in CLU》(CLU中的异常处理),是Barbara Liskov和Alan Snyder写的(IEEE Transactions on Software Engineering, Vol SE-5, No 6, Nov 1979)。遗憾的是,我不能在网上找到这篇文章的样本,只有一份纸质的副本。这有一份“A History of CLU”的论文,点击这里。
不幸的是,它没有在异常处理机制方面提供更多细节。值得注意的是,有大量信息的异常被看作是过程范式的扩展(因此也不是面向对象的必要的概念),CLU在其过程签名中支持抛出规则的平等,因此没有违反签名的过程体中的编译时检查(是一个有意识的设计决策,避免大量程序员纠结在无关紧要的细节上:->),未列出的异常自动转化为一个特殊的故障异常。
在这,你可以看到C++机制的起源,关键点被加强控制――有趣的是,因为没有编译时检查,拒绝了的最初的理论基础――像Java中的那样。CLU机制影响各处,也许最接近的结果在Modula3――这语言的报告在这有(注:好像原链接打不开了!!!)
有趣的是,Modula3也选择了抛出规格,相比较于编译时检查,更倾向于运行时。
Kevlin Henney
http://www.curbralan.com/
这里有一个C#设计者的有趣评论。重点部分:
小程序检查导致这样的结论,就是要求异常规范既可以提高开发者的积极性,也可以提高代码质量,但是大型软件项目的经验得出不一样的结论――降低积极性,极少几乎没有增强代码质量。
其余部分也在论述这个观点,我发现这特别引人注意的原因是,这个评论赞同预期异常似乎对小项目有用,这一点通常是我们争论点所在。然而,当项目越做越大时(实际上,我注意到时他们是任意样子的但是不小),预期异常变得不美观,并且看上去会引起问题。因此我认为预期异常之所以第一眼看上去令人信服、是对的的原因是他们在小例子中被展示和辩论。
讨论区