您的位置:首页 > 编程语言 > Java开发

Java学习笔记——通过异常处理错误

2013-09-05 13:00 615 查看
本文为Java编程思想第四版的学习笔记,在此感谢作者Bruce Eckel给我们带来这样一本经典著作,也感谢该书的翻译及出版人员。本文为原创笔记,转载请注明出处,谢谢。

1.概念

C语言以及其他早期语言常常具有多种错误处理模式,这些模式往往尽力在约定俗成的基础之上,而并不属于语言的一部分。通常会返回讴歌特殊值或者设置某个标志,并且假定接受者将对这个返回值或标志进行检查,以判定是否发生了错误。然而,随着时间的推移,人们发现,高傲的程序员们在使用程序库的时候更倾向于认为:“对,错误也许会发生,但那是别人造成的,不管我的事。正是由于程序员还仍然用这些方式拼凑系统,所以他们拒绝承认这样一个事实:对于构造大型、健壮、可维护的程序而言,这种错误处理模式已经成为了主要障碍。

解决的办法是,用墙纸规定的形式来消除错误处理过程中随心所欲的因素。这种做法由来已久,对异常处理的实现可追溯到20世纪60年代的操作系统,甚至于BASIC语言中的on error goto语句。而C++的异常处理机制基于Ada,Java中的异常处理则建立在C++的基础之上(尽管看上去使用更想Object Pascal).

”异常“这个词有”我对此感到意外“的意思。问题出现了,你也许不清楚该如何处理,但你的确知道不应该置之不理,你要停下来,看看是不是有别人或在别的地方,能够处理这个问题。只是在当前的环境中还没有足够的信息来解决这个问题,所以就把这个问题提交到一个更高级别的环境中,这里将作出正确的决定。使用异常所带来的另一个明显的好处是,它往往能够降低错误处理代码的复杂度。使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误。并且,只需在一个地方处理错误,即所谓的异常处理程序中。这不仅节省代码,而且吧”描述在正常执行过程中做什么事“的代码和”出了问题怎么办“的代码分离。

2.基本异常

异常情形是指组织当前方法或作用域继续执行的问题。把异常情形与普通问题相区分很重要,所谓的普通问题是指,在当前环境下能够的到足够的信息,总能处理这个错误。而对于异常情形,就不能继续下去了,因为在当前环境下无法获得必要的信息来解决问题。你所能做的就是从当前环境跳出,并且把问题提交给上一级环境。这就是抛出异常时所发生的事情。

当异常抛出时,有几件事会随之发生。首先,同Java中其他对象的创建一样,将使用new在堆上创建异常对象。然后,当前的执行路径(它不能继续执行下去了)被终止,并且从当前环境中弹出对异常对象的引用。此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务就是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。

异常使我们可以将每件事都当做一个事物来考虑,而异常可以看护这些事物的底线。我们还可以将异常看做是一种内建的恢复系统,因为(在细心使用的情况下)我们在程序中可以拥有各种不同的恢复点。如果程序的某部分失败了,异常将”恢复“到程序中某个已知的稳定点上。异常最重要的方面之一就是如果发生问题,他们将不允许程序沿着其正常的路径继续走下去。

2.1 异常参数

所有标准异常类都有两个构造器;一个是默认构造器;另一个是接受字符串作为参数,一边能把相关信息放入异常对象构造器。关键字throw将产生很多有趣的结果。在使用new创建了异常对象之后,此对象的引用将传给throw。能够抛出任何类型的Throwable对象,他是异常类型的跟类。通常,对于不同类型的错误,要抛出相应的异常。错误信息可以保存在异常对象内部或者用异常的名称来暗示。上一层环境通过这些信息来决定如何处理异常。

3.捕获异常

要明白异常是如何呗捕获的,必须首先理解监控区域的概念。他是一段可能产生异常的代码,并且后面跟着处理这些异常的代码。

3.1 try块

如果在方法内部抛出了异常(或者在方法内部调用的其他地方抛出了异常),这个方法将在抛出异常的过程中结束。要是不希望方法就此结束,可以在方法内设置一个特殊的块来捕获异常。因为这个块里”尝试“各种(可能产生异常的)方法调用,所以称为try块。

3.2 异常处理程序

当然,抛出的异常必须在某处得到处理。这个”地点“就是异常处理程序,而且针对每个要捕获的异常,得准备相应的处理程序。异常处理程序紧跟在try块之后,以关键字catch表示。每个catch子句看起来就像是接收一个且仅接受一个特殊类型的参数的方法。可以在处理程序内部使用标识符,这与方法参数的使用很相似。有时可能用不到标识符,因为异常的类型已经给了你足够的信息来对异常进行处理,但标识符并不可以省略。

异常处理程序必须紧跟在try块之后。当异常被抛出时,异常处理机制将负责搜索寻找参数与异常类型相匹配的第一个处理程序。然后进入catch子句执行,此时认为异常得到了处理。一旦catch子句结束,则处理程序的查找过程结束。注意,只有匹配的catch子句才能得到执行。注意在try块的内部,许多不同的方法可能会产生类型相同的异常,而你只需要提供一个针对此类型的异常处理程序。

3.3 终止与恢复

异常处理理论上有两种基本模型。Java支持终止模型(他是Java和C++所支持的模型)。在这种模型中,将假设错误非常关键,以至于程序无法返回到异常发生的地方继续执行。一旦异常被抛出,就表明错误已经无法挽回,也不能回来继续执行。

另一种称为恢复模型。意思是在异常处理程序的工作是修正错误,然后重新尝试调用出问题的方法,并认为第二次能成功。对于恢复模型,通常希望异常被处理之后能继续执行程序。如果想要用Java实现类似的恢复行为,那么在遇见错误时就不能抛出异常,而是调用方法来修正该错误。或者,把try块放在while循环里,这样就不断地进入try快,直到得到满意的结果。

长久以来以来,尽管从程序员们使用的操作系统支持恢复模型的异常处理,但他们最终还是转向使用类似”终止模型“的代码,并且忽略恢复行为。虽然恢复模型开始显得很吸引人,但不是很实用。其中的主要原因可能是它所导致的耦合,恢复性的处理程序需要了解异常抛出地点,这是比要包含依赖于抛出未知的非通用性代码。这增加了代码编写和维护的困难,对于异常可能会从愈多地方抛出的大型程序来说,更是如此。

4.创建自定义异常

不必拘泥于Java中已有的异常类型。Java提供的异常体系不可能预见所有的希望加以报告的错误,所以可以自己定义异常来表示程序中可能会遇到的特定问题。

要自己定义异常类,必须从已有的异常类继承,最好是选择意思相近的异常类继承(不过这样的异常并不容易找)。建立新的异常类型最简单的方法就是让编译器为你产生默认构造器,所以这几乎不用写多少代码。

4.1 异常与记录日志

可以使用java.util.logging工具将输出记录到日志中。这是一种很好的做法。

5.异常说明

Java鼓励人们把方法可能会抛出的异常告知使用此方法的客户端程序员。Java提供了相应的语法(并强制使用这个语法),使你能以礼貌的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。这就是异常说明,它属于方法声明的一部分,紧跟在形式参数列表之后。代码必须与异常说明一致。如果方法里的代码产生了异常却没有处理,编译器会发现这个问题并提醒你:要么处理异常,要么就在异常说明中表明此方法产生异常。通过这种自顶向下强制执行的异常说明机制,Java在编译时就可以保证一定水平的异常正确性。

不过还是有个能”作弊“的地方:可以声明方法抛出异常,实际上却不抛出。编译器相信了这个声明,并强制此方法的用户像真的抛出异常那样使用这个方法。这样做的好处是,为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时,这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。这种在编译时被强制检查的异常称为被检查的异常。

6.捕获所有异常

可以执行一个异常处理程序来捕获所有类型的异常。通过捕获异常类型的基类Exception,你就可以做到这一点(事实上还有其他的基类,但Exceptiong是同变成活动相关的基类)。这种捕获所有异常的代码,最好把它放在处理程序列表的末尾,以防止它抢在其他处理程序之前先把异常捕获了。

6.1 栈轨迹

printStackTrace()方法所提供的信息可以通过getStackTrace()方法来直接访问,这个方法将返回一个由栈轨迹中的元素所构成的数组,其中每一个元素都表示栈中的一桢。元素0是栈顶元素,并且是调用序列中最后一个方法调用(这个Throwable被创建和抛出之处)。数组中的最后一个元素和栈底是调用序列中的第一个方法调用。

6.2 重新抛出异常

有时希望把刚捕获的异常重新抛出,尤其是在使用Exception捕获所有异常的时候。既然已经得到了对当前异常对象的引用,可以直接把它重新抛出。重新抛出异常会把异常抛给上一级环境中的异常处理程序,同一个try块后续catch子句将被忽略此外,异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。

如果只是把当前对象重新抛出,那么printStackTrace()方法将显示原来异常抛出点的调用信息,而并非重新抛出点的信息。想要更新这个信息,可以调用fillInStackTrack()方法,这将返回一个Throwable对象,他是通过把当前调用栈信息填入原来那个异常对象而建立的。

有可能在捕获异常之后抛出另一种异常。这么做的话,得到的效果类似于使用fillInStackTrace(),有关原来异常发生点的信息会丢失,剩下的是新的抛出点有关的信息。

6.3 异常链

常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为异常链。在JDK 1.4以前,程序员必须自己编写代码来保存原始异常的信息。现在所有Throwable的子类在构造器中都可以接受一个cause对象作为参数。这个cause就用来表示原始异常,这样通过把原始异常传递给新的异常,就使得即使哎当前位置创建并抛出新的异常,也能铜鼓这个异常链追踪到异常最初发生的位置。在Throwable的子类中,只有三种基本的异常类提供了带cause参数的构造器。它们是Error,Exception和RuntimeException。如果要把其他类型的异常链接起来,应该使用initCause()方法而不是构造器。

7.Java标准异常

Throwable这个Java类被用来表示任何可以作为异常被抛出的类。Throwable对象可分为两种类型(指从Throwable继承而得到的类型):Error用来表示编译时和系统错误(除特殊情况外,一般不用你关心);Exception是可以被抛出的基本类型,在Java类库、用户方法以及运行时故障中都可以抛出Exception型异常。

7.1 特例:RuntimeException

RuntimeException是Java的标准运行时检测的一部分。它们会被Java虚拟机抛出,所以不必在异常说明中把它们列出来。这构成了一组具有相同特征和行为的异常类型。并且,也不再需要在异常说明中声明方法将抛出RuntimeException类型异常,它们被称为“不受检查异常”。这种异常属于错误,将被自动捕获。

8.使用finally进行清理

对于一些代码,可能会希望无论try块中的异常是否抛出,它们都能得到执行。这通常适用于内存回收之外的情况(因为回收由垃圾回收器完成)。为了达到这个效果,可以在异常处理程序后面加上finally子句。

8.1 finally用来做什么

当要把除内存之外的资源恢复到他们的初始状态时,就要用到finally子句。这种需要清理的资源包括:已经打开的文件或网络连接,在屏幕上画的图形,甚至可以是外部世界的某个开关。

8.2 在return中使用finally

因为finally子句总是会执行的,所以在一个方法中,可以从多个点返回,并且可以保证重要的清理工作仍旧会执行。

8.3 缺憾:异常丢失

遗憾的是,Java的异常实现也有瑕疵。异常作为程序出错的标志,绝不应该被忽略,但它还是有可能被轻易地忽略。用某些特殊的方式使用finally子句,就会发生这种情况。

9.异常的限制

当覆盖方法的时候,只能抛出在基类方法的异常说明里列出的哪些异常。这个限制很有用,因为这意味着,当基类使用的代码应用到派生类对象的时候,一样能够工作(当然,这是面向对象的基本概念),异常也不例外。

10.构造器

有一点很重要,即你要时刻询问自己“如果异常发生了,所有东西能被正确的清理吗“?尽管大多数情况下是非常安全的,但涉及构造器时,问题就出现了。构造器会啊对象设置成安全的初始状态,但还会有别的动作,比如打开一个文件,这样的动作只有在对象使用完毕并且用户调用了特殊的清理方法之后才能得以清理。如果在构造器内抛出了异常,这清理行为也许就不能正常工作。这意味着在编写构造器是要格外的小心。

读者也许会认为使用finally就可以解决问题。但问题并非如此简单,因为finally会每次都执行清理代码。如果构造器在执行过程中半途而废,也许该对象的某些部分还没有被成功创建,而这些部分在finally子句中确是要被清理的。

对于在构造阶段可能会抛出异常,并且要求清理的类,最安全的使用方式是使用嵌套的try。这种通用的清理习惯在构造器不抛出任何异常时也应该运用,其基本规则是:在创建需要清理的对象之后,立即进入一个try-finally语句块。

11.异常匹配

抛出异常的时候,异常处理系统会按照代码的书写顺序找出”最近“的吹程序。找到匹配的处理程序之后,他就认为异常将得到处理,然后就不再继续查找。

查找的时候不要求抛出的异常同处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序。如果把捕获积累的catch子句放在最前面,就会把派生类的异常全给”屏蔽“掉。

12.其他可选方式

这个地方很深奥,是作者的深层次的探讨和研究。以后再学吧。

13.异常使用指南

应该在下列情况下使用异常:

1)在恰当的级别处理问题(在知道该如何处理的情况下才捕获异常)。

2)解决问题并且重新调用产生异常的方法。

3)进行少许修补,然后绕过异常发生的地方继续执行

4)用别的数据进行计算,以代替方法预计会返回的值。

5)把当前运行环境下能做的事情尽量做完,然后把相同的异常重抛到更高层。

6)把当前运行环境下能做的事情尽量做完,然后把不同的异常抛到更高层。

7)终止程序。

8)进行简化。

9)让类库和程序更安全。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: