Java编程思想(第四版)—— 读书笔记(十一)

第12章    通过异常处理错误

    Java基本理念是“结构不佳的代码不能运行”。改进的错误恢复机制是提供代码健壮性的最强有力的方式。

一、概念

1、用强制规定的形式来消除错误处理过程中随心所欲的因素。

2、异常能够降低错误处理代码的复杂度。只需在一个地方处理错误(即所谓的异常处理程序中)。这种方式不仅节省代码,而且把“描述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。

二、基本异常

1、异常情形:指阻止当前方法或作用域继续执行的问题。

2、普通问题:在当前环境下能得到足够的信息,总能处理这个错误。

3、当抛出异常后,有几件事会随之发生。

(1)首先,同Java中其他对象的创建一样,将使用new在堆上创建异常对象,也伴随着存储空间的分配和构造器的调用。(所有标准异常类都有两个构造器:一个是默认构造器;另一个是节后字符串作为参数,以便能把相关信息放入异常对象的构造器)。

图1

(2)然后,当前的执行路径(它不能继续下去了)被终止,并且从当前环境中弹出对异常对象的引用。

(3)此时,异常处理机制接管程序,并开始寻找一个恰当的地方来继续执行程序。这个恰当的地方就是异常处理程序,它的任务是将程序从错误状态中恢复,以使程序能要么换一种方式运行,要么继续运行下去。

图2

4、异常可以将每一件事当做一个事务来考虑,而异常可以看护这些事务的底线。

5、异常允许强制程序停止运行,并告诉我们出现了什么问题,或者(理想状态下)强制程序处理问题,并返回到稳定状态。

6、所有标准异常类都有两个构造器:一个是默认构造器,另一个是接收字符串作为参数。

图3

三、捕获异常

1、监控区域:一段可能产生异常的代码,并且后面跟着处理这些异常的代码。

2、try块

图4

3、异常处理程序:抛出的异常必须在某处得到处理,而且针对每个要捕获的异常,得准备相应的处理程序。

图5

4、注意try块的内部,许多不同的方法调用可能产生类型相同的异常,而只需要提供一个针对此类型的异常处理程序。

5、Java支持终止模式、恢复模式。

7、在遇见错误时候就不能抛出异常,而是调用方法来修正该错误。把try块放在while循环里,这样就不断地进入try块,知道得到满意的结果。

8、恢复模型不是很实用:因为可能是它所导致的耦合:恢复性的处理程序需要了解异常抛出的地点,这势必要包含依赖于抛出位置的非通用性代码。

四、创建自定义异常

1、要自己定义异常类,必须从已有的异常类继承。建立新的异常类型最简单的方法就是让编译器为你产生默认构造器。

图6

2、标准错误流:System.err。通常这比把错误信息输出到System.out要好,因为System.out也许会被重定向。如果把结果送到System.err,它就不会随System.out一起被重定向,这样更容易被用户注意。

图7

3、e.printStackTrace();信息将被输出到标准错误流。

图8

4、getMessage()方法类似于toString()方法。

5、只是查看一下抛出的异常类型,对异常所添加的其他功能也许根本用不上。

五、异常说明

1、throw。以礼貌的方式告知客户端程序员某个方法可能会抛出的异常类型,然后客户端程序员就可以进行相应的处理。这就是异常说明,它属于方法声明的一部分,紧跟在形式参数列表之后。

2、关键字 throws,后面接一个所有潜在异常类型的列表。

图9

3、要么处理这个异常,要么就在异常说明中表明此方法将产生异常。通过这种自顶向下强制执行的异常说明机制,Java在编译时就可以保证一定水平的异常正确性。

4、可以声明方法将抛出异常,实际上却不抛出。为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码。在定义抽象基类和接口时这种能力很重要,这样派生类或接口实现就能够抛出这些预先声明的异常。

5、被检查的异常:被强制检查的异常。

六、捕获所有异常

1、捕获异常类型的基类Exception。捕获所有异常,最好把它放在处理程序列表的末尾。

图10

2、Throwable继承的方法:String getMessage()、String getLocalizedMessage()用来获取详细信息,或用本地语言表示的详细信息。

     String toString()返回对Throwable的简单描述,要是有详细信息的话,也会把它包含在内。

     void printStackTrace()、void printStackTrace(PrintStream)、void printStackTrace (java.io.PrintWriter)打印Throwable和Throwable的调用栈轨迹。

     Throwable fillInStackTrace()用于在Throwable对象的内部记录栈帧的当前状态。在程序重新抛出错误异常时很有用。

     使用Throwable从基类Object继承方法。对于异常,getClass()也许是个很好用的方法,将返回一个表示此对象类型的对象。getName()方法查询这个Class对象包含信息的名称,或者使用类名称的getSimpleName()方法。

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

4、重抛异常会把异常抛给上一级环境中的异常处理程序,异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。

     只是把当前异常对象重新抛出,printStackTrace()方法,这将返回一个Throwable对象,它是通过把当前调用栈信息填入原来那个异常对象而建立的。

     永远不必为清理前一个异常对象而担心,或者说为异常对象的清理而担心。他们都是用new在堆上创建对象,所以垃圾回收器会自动把他们清理掉。

5、异常链:

七、Java标准异常

1、Throwable用来表示任何可以作为异常被抛出的类。分为两种类型:Error用来表示编译时和系统错误、Exception是可以被抛出的基本类型(经常关心的是这个)。

2、异常并非全是在java.lang包里定义的。

3、RuntimeException:运行时异常。不受检查异常。尽管不用捕获,但是可以抛出。

     NullPointerException:对null引用进行调用。

4、如果不捕获这种类型的异常会发生什么事呢?因为编译器没有在这个问题上对异常说明进行强制检查,RuntimeException类型的异常也许会穿越所有的执行路径直达main()方法,而不会被捕获。

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

    (1)无法预料的错误。比如从你控制范围之外传递进来的null引用。

    (2)作为程序员,应该在代码中进行检查的错误。在一个地方发生异常,常常会在另一个地方导致错误。

八、使用finally进行清理

1、在异常处理程序后面加上finally字句。finally总能被执行。

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

3、程序的目的就是要确保main()结束的时候开关必须是关闭的,所以在每个try块和异常处理程序的末尾都加入对sw.off()方法的调用。

图11

4、当涉及到break和continue语句的时候,finally字句也会得到执行。请注意,如果把finally字句和带标签的break及continue配合使用,在Java里没必要使用goto语句了。

5、在return中使用finally:

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

     在finally类内部,从何处返回无关紧要。

6、缺憾:异常丢失。

九、异常的限制

1、当基类使用的代码应用到其派生类对象的时候,一样能够工作,异常也不例外。

2、异常限制对构造器不起作用。因为基类构造器必须以这样或那样的方式被调用(这里默认构造器九江自动被调用),派生类构造器的异常说明必须包含基类构造器的异常说明。

3、派生类构造器不能捕获基类构造器抛出的异常。

4、通过强制派生类遵守基类方法的异常说明,对象的可替换性得到了保证。

十、构造器

1、除了内存的清理之外,所有的清理不会自动发生。

2、在创建需要清理的对象之后,立即进入一个try-finally语句块。

十一、异常匹配

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

2、查找的时候并不要求抛出的异常同处理程序所声明的异常完全匹配。派生类的对象也可以匹配其基类的处理程序。

3、如果把捕获基类的catch字句放在最前面,以此把派生类的异常全给“屏蔽”掉。

图12

十二、其他可选方式

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

2、异常处理的一个重要原则是“只有在你知道如何处理的情况下才捕获异常”。

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

4、“被检查的异常”,强制你在可能还没准备好处理错误的时候被迫加上catch字句,这就导致了吞食则有害的问题:

图13

5、“被检查的异常”和强静态类型检查对开发不是必要。动态语言的好处: 

    1)不在于编译器是否会强制程序员去处理错误,而是要有一致的、使用异常来报告错误的模型。

    2)不在于什么时候进行检查,而是一定要有类型检查。也就是说,必须强制程序使用正确的类型,至于这种强制施加于编译时还是运行时,那倒没关系。

    事实上,反射和泛型就是用来补偿静态类型检查所带来的过多限制。

6、把异常传递给控制台。

7、把“被检查的异常”转换为“不检查的异常”

十三、异常使用指南

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

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

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

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

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

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

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

    7)终止程序。

    8)进行简化。(如果你的异常模式使问题变得太复杂,那用起来会非常痛苦也很烦人。)

    9)让类库和程序更安全。(这既是在为调试做短期投资,也是在为程序的健壮性做长期投资。)


你可能感兴趣的:(Java编程思想(第四版)—— 读书笔记(十一))