Java编程思想9——使用异常处理错误(Error Handling with Exceptions)

Exception这个词表达的是一种“例外”情况,亦即正常情况之外的一种“异常”。在问
题发生的时候,我们可能不知具体该如何解决,但肯定知道已不能不顾一切地继续下去。此时,必须坚决地停下来,并由某人、某地指出发生了什么事情,以及该采取何种对策。但为了真正解决问题,当地可能并没有足够多的信息。因此,我们需要将其移交给更级的负责人,令其作出正确的决定(类似一个命令链)。

异常机制的另一项好处就是能够简化错误控制代码。我们再也不用检查一个特定的错误,然后在程序的多处地方对其进行控制。此外,也不需要在方法调用的时候检查错误(因为保证有人能捕获这里的错误)。我们只需要在一个地方处理问题:“异常控制模块”或者“异常控制器”。这样可有效减少代码量,并将那些用于描述具体操作的代码与专门纠正错误的代码分隔开。一般情况下,用于读取、写入以及调试的代码会变得更富有条理。

1.基本异常

“异常条件”表示出现应中止方法或作用域继续的问题。将异常条件与普通问题区分开是非常重要的。在普通问题的情况下,我们在当地已拥有足够的信息,可在某种程度上解决碰到的问题。而在异常条件的情况下,却无法继续下去,因为当地没有提供解决问题所需的足够多的信息。

产生一个异常时,会发生几件事情。首先,按照与创建Java对象一样的方法创建异常对象:在内存“堆”里,使用new来创建。随后,停止当前执行路径(记住不可沿这条路径继续下去),然后从当前的环境中抛出异常对象。此时,异常控制机制会接管一切,并开始查找一个恰当的地方,用于继续程序的执行。这个恰当的地方便是“异常处理器”(exception handler),它的职责是从问题中恢复,使程序要么尝试另一条执行路径,要么简单地继续。

1.1 异常自变量

在所有标准异常中,存在着两个构建器:第一个是默认构建器,第二个则需使用一个字串自变量,使我们能在异常里置入相关信息。

可根据需要掷出任何类型的“可掷”对象。典型情况下,我们要为每种不同类型的错误“掷”出一类不同的异常。我们的思路是在异常对象以及挑选的异常对象类型中保存信息,所以在更大场景中的某个人可知道如何对待我们的异常(通常,唯一的信息是异常对象的类型,而异常对象中保存的没什么意义)。

2.异常的捕获

若某个方法产生一个异常,必须保证该异常能被捕获,并获得正确对待。对于Java的异常处理机制,它的一个好处就是允许我们在一个地方将精力集中在要解决的问题上,然后在另一个地方对待来自那个代码内部的错误。

2.1 try块

若不想throw后直接离开方法,可在那个方法内部设置一个特殊的代码块,用它捕获异常。这就叫作“try块”。

2.2 异常处理器

当然,生成的异常必须在某个地方中止。这个“地方”便是异常处理器或者异常处理模块。而且针对想捕获的每种异常类型,都必须有一个相应的异常处理器。异常处理器紧接在try块后面,且用catch(捕获)关键字标记。

处理器必须“紧接”在try块后面。若“掷”出一个异常例,异常处理机制就会搜寻自变量与异常类型相符的第一个处理器。随后,它会进入那个catch从句,并认为异常已得到处理(一旦catch从句结束,对处理器的搜索也会停止)。

2.2.1 中断与恢复

在异常处理理论中,共存在两种基本方法。

  • 1)中断
    假定错误非常关键,没有办法返回异常发生的地方。无论谁只要“掷”出一个异常,就表明没有办法补救错误,而且也不希望再回来。
  • 2)恢复
    意味着异常处理器有责任来纠正当前的状况,然后取得出错的方法,假定下一次会成功执行。若使用恢复,意味着在异常得到处理以后仍然想继续执行。

2.3 异常规范

异常规范采用了一个额外的关键字:throws;后面跟随全部潜在的异常类型。

2.4 捕获所有异常

可创建一个处理器,令其捕获所有类型的异常。具体的做法是捕获基础类异常类型Exception。
这段代码能捕获任何异常,所以在实际使用时最好将其置于处理器列表的末尾,防止跟随在后面的任何特殊异常处理器失效。

由于Exception类是它们的基础,所以我们不会获得关于异常太多的信息,但可调用来自它的基础类Throwable的方法。

2.5 重新抛出异常

在某些情况下,我们想重新掷出刚才产生过的异常,特别是在用Exception捕获所有可能的异常时。由于我们已拥有当前异常的句柄,所以只需简单地重新掷出那个句柄即可。

若只是简单地重新掷出当前异常,我们打印出来的、与printStackTrace()内的那个异常有关的信息会与异常的起源地对应,而不是与重新掷出它的地点对应。若想放置新的堆栈跟踪信息,可调用fillInStackTrace(),它会返回一个特殊的异常对象:将当前堆栈的信息填充到原来的异常对象里。

3.标准Java异常

Java包含了一个名为Throwable的类,它对可以作为异常“掷”出的所有东西进行了描述。Throwable对象有两种常规类型(亦即“从Throwable继承”)。其中,Error代表编译期和系统错误,我们一般不必特意捕获它们(除在特殊情况以外)。Exception是可以从任何标准Java库的类方法中“掷”出的基本类型。此外,它们亦可从我们自己的方法以及运行期偶发事件中“掷”出。

3.1 特例:RuntimeException

看起来似乎在传递进入一个方法的每个句柄中都必须检查null(因为不知道调用者是否已传递了一个有效的句柄),这无疑是相当可怕的。但幸运的是,我们根本不必这样做——它属于Java进行的标准运行期检查的一部分。若对一个空句柄发出了调用,Java会自动产生一个NullPointerException异常。

由于它们用于指出编程中的错误,所以几乎永远不必专门捕获一个“运行期异常”——RuntimeException——它在默认情况下会自动得到处理。

如果不捕获这些异常,又会出现什么情况呢?由于编译器并不强制异常规范捕获它们,所以假如不捕获的话,一个RuntimeException可能渗透到main()方法。

假若一个RuntimeException在没有捕获异常的情况下直达main(),那么当程序退出时,会为那个异常调用printStackTrace()。

请务必记住:只能在代码中忽赂RuntimeException(及其子类)类型的异常,其他类型异常的处理都是由编译器强制实施的。究其原因,RuntimeException代表的是编程错误:

  • 1)无法预料的错误。比如从你控制范围之外传递进来的null引用。
  • 2)作为程序员,应该在代码中进行检查的错误。(比如对于ArraylndexOutOf­BoundsException, 就得注意一下数组的大小了。)在一个地方发生的异常,常常会在另一个地方导致错误。

4.使用finally进行清理

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

4.1 何时使用finally

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

4.2 在return中使用finally

4.3 缺憾:异常丢失

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

5.异常的限制

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

异常限制对构造器不起作用。然而,因为基类构造器必须以这样或那样的方式被调用(这里1ill1 默认构造器将自动被调用),派生类构造器的异常说明必须包含基类构造器的异常说明。
派生类构造器不能捕获基类构造器抛出的异常。

如果处理的刚好是StormyInning对象的话,编译器只会强制要求你捕获这个类所抛出的异常。但是如果将它向上转型成基类型,那么编译器就会(正确地)要求你捕获基类的异常。所有这些限制都是为了能产生更为强壮的异 常处理代码。

尽管在继承过程中,编译器会对异常说明做强制要求,但异常说明本身并不属于方法类型的一部分,方法类型是由方法的名字与参数的类型组成的。因此,不能基千异常说明来重载方法。

此外,一个出现在基类方法的异常说明中的异常,不一定会出现在派生类方法的异常说明匝习 里。这点同继承的规则明显不同,在继承中,基类的方法必须出现在派生类里,换句话说,在继承和覆盖的过程中,某个特定方法的“异常说明的接口”不是变大了而是变小了一这恰好和类接口在继承时的情形相反。

6.构造器

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

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

7.异常匹配

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

查找的时候井不要求抛出的异常同处理程序所声明的异常完全匹配。 派生类的对象也可以匹配其基类的处理程序。
如果决定在方法里加上更多派生异常的话,只要客户程序员捕获的是基类异常,那么它们的代码就无需更改。

8.其他可选方式

异常代表了当前方法不能继续执行的情形。 开发异常处理系统的原因是,如果为每个方法所有可能发生的错误都进行处理的话, 任务就显得过于繁重了,程序员也不愿意这么做。结果常常是将错误忽略。 应该注意到,开发异常处理的初衷是为了方便程序员处理错误。

异常处理的一个重要原则是 “只有在你知道如何处理的情况下才捕获异常”。实际上, 异常处理的一个重要目标就是把错误处理的代码同错误发生的地点相分离。 这使你能在一段代码中专注于要完成的事情,至于如何处理错误,则放在另一段代码中完成。 这样以来,主干代码就不会与错误处理逻辑混在一起,也更容易理解和维护。 通过允许一个处理程序去处理多个出错点, 异常处理还使得错误处理代码的数盐趋向于减少。

”被检查的异常“ 使这个问题变得有些复杂,因为它们强制你在可能还没准备好处理错误的时候被迫加上catch子句,这就导致了吞食则有害 (harmful if swallowed) 的问题。异常确实发生了,但 “吞食” 后它却完全消失了。

只有当Java修改了它那类似C++的模型,使异常成为报告错误的唯一方式, 那时 “被检查的异常” 的额外限制也许就会变得没有那么必要了。

1)不在于编译器是否会强制程序员去处理错误,而是要有一致的、 使用异常来报告错误的模型。
2)不在千什么时候进行检查, 而是一定要有类型检查。也就是说, 必须强制程序使用正确的类型, 至干这种强制施加于编译时还是运行时,那倒没关系。

此外,减少编译时施加的约束能显著提高程序员的编程效率。事实上, 反射和泛型就是用来补偿静态类型检查所带来的过多限制。

果发现有些 ”被检查的异常” 挡住了路, 尤其是发现你不得不去对付那些不知道该如何处理的异常,还是有些办法的。

  • 1)把异常传递给控制台
  • 2)把 “被检查的异常“ 转换为 “不检查的异常”:以直接把“被检查的异常“包装进RuntiJneException里面

9.异常使用指南

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

  • 1)在恰当的级别处理问题。(在知道该如何处理的情况下才捕获异常。)
  • 2)解决问题并且重新调用产生异常的方法。
  • 3)进行少许修补, 然后绕过异常发生的地方继续执行。
  • 4)用别的数据进行计算, 以代替方法预计会返回的值。
  • 5)把当前运行环境下能做的事情尽量做完, 然后把相同的异常重抛到更高层。
  • 6)把当前运行环境下能做的事情尽量做完, 然后把不同的异常抛到更高层。
  • 7)终止程序。
  • 8)进行简化。(如果你的异常模式使问题变得太复杂, 那用起来会非常痛苦也很烦人。)
  • 9)让类库和程序更安全。(这既是在为调试做短期投资, 也是在为程序的健壮性做长期投资。

10.总结

异常处理的优点之一就是它使得你可以在某处集中档力处理你要解决的问题, 而在另一处处理你编写的这段代码中产生的错误。

尽管异常通常被认为是一种工具, 使得你可以在运行时报告错误并从错误中恢复, 但是我一直怀疑到底有多少时候 “恢复“ 真正得以实现了, 或者能够得以实现。 我认为这种情况少千10%, 并且即便是这10%, 也只是将栈展开到某个已知的稳定状态, 而井没有实际执行任何种类的恢复性行为。

无论这是否正确, 我一直相信 “报告” 功能是异常的精髓所在。Java坚定地强调将所有的错误都以异常形式报告的这一事实, 正是它远远超过诸如C++这类语言的长处之一,因为在C++这类语言中, 需要以大量不同的方式来报告错误, 或者根本就没有提供错误报告功能。 一致的错误报告系统意味着, 你再也不必对所写的每段代码,都质问自己“错误是否正在成为漏网之鱼?”(只要你没有“吞咽“异常,这是关键所在!)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • packagetestexcrpltiom; importjava.text.ParseException; im...
    猿学阅读 1,399评论 0 2
  • 引言 在程序运行过程中(注意是运行阶段,程序可以通过编译),如果JVM检测出一个不可能执行的操作,就会出现运行时错...
    Steven1997阅读 2,329评论 1 6
  • 今天早上又尝试了化妆,感觉自己确实精神好看了(。・ω・。)ノ,喜欢这种感觉,以后有空了就多练一下手,就是双眼皮贴感...
    菏泠阅读 104评论 0 0
  • 2018.03.01 暗香幽来 昨日,我还在春寒料峭、万物凋蔽的北方,今天已在春风化雨、繁华盛开的闽南风景...
    流年染墨香阅读 291评论 3 1
  • 第一次接触越野赛,是从志愿者开始的。 多少次挑战通宵,然而最完整的挑战通宵却是从这次TNF100实现的。 周六下午...
    Halilisa阅读 107评论 0 0