Java—异常的捕获与处理

异常的基本概念

正如“天有不测风云,人有旦夕祸福”。Java的程序代码在一些特定环境下,也会发生某些不测情况。在编程过程中,首先应当尽可能去避免错误和异常发生,对于不可避免、不可预测的情况则要考虑异常发生时如何处理。

异常(Exception)也称为例外,指的是所有可能造成计算机无法正常处理的情况,如果没有实现妥善的安排,严重的话可以使得计算机死机。异常处理是一种特定的程序错误处理机制,是为了让程序员更加关注正常的程序执行序列而设计的。异常处理提供了一种标准的方法以处理错误,发现可预知及不可预知的问题,及允许开发者识别、查出和修改错漏之处。

处理错误的方法有如下几个特点:

⑴ 不需要打乱程序的结构,如果没有任何错误产生,那么程序的运行不受任何影响。

⑵ 不依靠方法的返回值来报告错误是否产生。

⑶ 采用集中的方式处理错误,能够根据错误种类的不同来进行对应的错误处理操作。

下面列出的是Java中几个常见的异常,括号内所注的英文是对应的异常处理类名称。

⑴ 算术异常(ArithmeticException):当算术运算中出现了除以零这样的运算就会出这样的异常。

⑵ 空指针异常(NullPointerException):没有给对象开辟内存空间却使用该对象时会出现空指针异常。

⑶ 文件未找到异常(FileNotFoundException):当程序试图打开一个不存在的文件进行读写时将会引发该异常。经常是由于文件名给错,或者要存储的磁盘、CD-ROM等被移走,没有放入等原因造成。

⑷ 数组下标越界异常(ArrayIndexOutOfBoundsException):对于一个给定的大小的数组,如果数组的索引超过上限或低于下限都造成越界。

⑸ 内存不足错误(OutOfMemoryException):当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。

Java的异常处理机制也秉承着面向对象的基本思想。在Java中,所有的异常都是以类的类型存在。除了内置的异常类之外,Java也可以自定义异常类。此外,Java的异常处理机制也允许自定义抛出异常。

为何需要异常处理

异常是在程序运行过程中发生的事件,比如除以零溢出、数组越界、文件找不到等,这些事件的发生将阻止程序的正常运行。为了加强程序的健壮性(Robust),在程序设计时,必须考虑到可能发生的异常事件,并做出相应的处理。在C语言中,可通过使用if…else…语句控制异常。同时,可通过被调用函数的返回值感知在其中产生的异常事件,并进行处理。然而,使用函数返回的全局ErroNo(错误代码),仅能用来反映一个异常事件的类型。这种异常监控模式非常繁琐,同一个异常或者错误如果多个地方出现,那么在每个地方都要做相同处理。

在程序编制过程中,有一个80/20原则, 即80%的精力花费在20%的事情上,而这20%的事情就是处理各种可能出现的错误或异常。如果想编制一个完善的高容错运行程序, 且没有使用异常处理机制的话, 程序中将充斥着if语句,如果这样的话,整个程序的结构就会变得臃肿而混乱。而事实上,由于程序员本身存在思维盲点,即使再简单的程序,要把其所有可能出现错误都预想到也是不现实的。

Java通过面向对象的方法来处理异常。在一个方法的运行过程中,如果发生了异常,则这个方法生成代表该异常的一个对象,并把它交给运行时系统,运行时系统寻找相应的代码来处理这一异常。我们把生成异常对象并把它提交给运行时系统的过程称之为异常的抛出(throw)。运行时系统在方法的调用栈中查找,从生成异常的方法开始进行回溯,直到找到包含相应异常处理的方法为止,这一过程称之为异

简单的异常范例

Java本身已有较为完善的机制来处理异常的发生。下面先来看看Java是如何处理异常的,TestException是一个错误的程序,它在访问数组时,下标值已超过了数组下标所允许的最大值,因此会有异常发生。

数组越界异常范例(TestException.java)。
Java—异常的捕获与处理_第1张图片
在编译的时候程序不会发生任何错误,但是在执行到第06行时,数组的下标为10,超过了arr数组所能允许的下标最大值4,于是,就会产生如下图所示的错误信息。
在这里插入图片描述
异常产生的原因在于,数组的下标值超出了最大允许的范围。Java检测这个异常之后,便由系统抛出“ArrayIndexOutOfBoundsException”,用来表示错误的原因,并停止运行程序。如果没有编写相应的处理异常的程序代码,Java的默认异常处理机制会先抛出异常,然后停止运行程序。

发现,在出现异常之后,异常语句之后的代码将不再执行,而是直接结束了程序的运行,那么就表示此时程序是处于一种“不健康”的状态。为了保证程序出现异常之后,依然可以“善始善终”地完结,就需要引入异常处理操作。

异常的处理

在【范例 TestException.java】的异常发生后,Java便把这个异常抛了出来,可是抛出来之后没有程序代码去捕捉它,所以程序到第6行便结束,因此根本不会执行到第7行。如果加上捕捉异常的程序代码,则可针对不同的异常做妥善的处理,这种处理的方式称为异常处理

异常处理是由try、catch与finally等3个关键字所组成的程序块,其语法如下所示(方括号内的部分是可选部分)。
Java—异常的捕获与处理_第2张图片
Java提供了try(尝试)、catch(捕捉)及finally(最终)这3个关键词来处理异常。这3个动作描述了异常处理的3个流程。

⑴ 首先,我们把所有可能发生异常的语句都放到一个try之后由{ }所形成的区块称为“try区块(”try block)。程序通过try{}区块准备捕捉异常。try程序块若有异常发生,程序的运行便中断,并抛出“异常类所产生的对象”。

⑵ 抛出的对象如果属于catch()括号内欲捕获的异常类,catch则会捕捉此异常,然后进入catch的块里继续运行。

⑶ 无论try程序块是否捕捉到异常,或者捕捉到的异常是否与catch()括号里的异常相同,最终一定会运行finally块里的程序代码。

finally的程序代码块运行结束后,程序再回到try-catch-finally块之后继续执行。

由上述的过程可知,在异常捕捉的过程中至少做了两个判断:第1个是try程序块是否有异常产生,第2个是产生的异常是否和catch()括号内欲捕捉的异常相同。

值得一提的是,finally块是可以省略的。如果省略了finally块,那么在catch()块运行结束后,程序将跳到try-catch块之后继续执行。

根据这些基本概念与运行的步骤,可绘制出下图所示的流程。
Java—异常的捕获与处理_第3张图片
从上面的流程图可以看出,异常处理格式之中可以分为三类:try{}…catch{}、try{}…catch{}…finally{}、try{}…finally{}。

在处理各种异常,需要用到对应的“异常类”,“异常类”指的是由程序抛出的对象所属的类。例如,【范例 TestException.java】中出现的“ArrayIndexOutOfBoundsException”就是众多异常类的一种。异常类以及它们之间的继承关系,稍后我们会做进一步的探讨。下面的程序代码是对【范例19-1】的改善,其中加入了try与catch,使得程序本身具有了捕捉异常与处理异常的能力,这样当程序发生数组越界异常,也能保证程序以可控的方式运行。

异常处理的使用(DealException.java)。
Java—异常的捕获与处理_第4张图片
Java—异常的捕获与处理_第5张图片
第08行声明了一个名为arr的数组,并开辟了一个包含5个整型数据的内存空间,由于数组的下标是从0开始计数的,显然,数组arr能允许的最大下标为4。

第09行尝试为数组中的第11个元素赋值,此时已经超出了该数组所能控制的范围,所以在运行时会发生数组越界异常。发生异常之后,程序语句转到catch语句中去处理,最后程序通过finally代码块统一结束。

程序的第05~10行的try块是用来检查花空号{}内是否会有异常发生。若有异常发生,且抛出的异常是属于ArrayIndexOutOfBoundsException类型,则会运行第11~14行的代码块。因为第8行所抛出的异常正是ArrayIndexOutOfBoundsException类,因此第12行会输出“数组超出绑定范围!”字符串。由本例可看出,通过异常处理机制,即使程序运行时发生问题,只要能捕捉到异常,程序便能顺利地运行到最后,而且还能适时地加入对错误信息的提示。

第11行,如果程序捕捉到了异常,则在catch括号内的异常类ArrayIndexOutOfBoundsException之后生成一个对象ex,利用此对象可以得到异常的相关信息。下例说明了异常类对象ex的应用。

异常类对象ex的使用(EObject.java)。
Java—异常的捕获与处理_第6张图片
Java—异常的捕获与处理_第7张图片
第12行,输出了所捕获的异常对象ex。同时,注意到本例省略了finally{}代码块。finally{}代码块的本意是程序运行的最后的“打完收工”步骤,而这事实上属于非必需的,所以本例没有提供这个代码块。

在第10行中,可以把catch()视为一个专门捕获异常的方法,而括号内的内容可视为方法的参数,而ex就是ArrayIndexOutOfBoundsException类所实例化的对象。对象ex接收到由异常类所产生的对象之后,就进到第11行,输出“数组超出绑定范围!”这一字符串,然后在第12行输出异常所属的种类—java.lang.ArrayIndexOutOfBoundsException,其中java.lang是ArrayIndexOutOfBoundsException类所属的包。

值得注意的是,如果想得到详细的异常信息,则需要使用异常对象的printStackTrace()方法。例如,如果我们在第12行后增加如下代码。
在这里插入图片描述
则运行的结果,如下图所示。
Java—异常的捕获与处理_第8张图片
由运行结果可以看出,printStackTrace()方法给出了更为详细的异常信息,不仅包括异常的类型,还包括异常发生在哪个所属包、哪个所属类、哪个所属方法以及发生异常的行号。

以上的范例演示的是进行一个异常的操作处理,而事实上在一个try语句之后实际上可以跟上多个异常处理catch语句,来处理多种不同类型的异常。请观察下面的范例。

通过初始化参数传递操作数字,使用多个catch捕获异常。
Java—异常的捕获与处理_第9张图片
Java—异常的捕获与处理_第10张图片
Java—异常的捕获与处理_第11张图片
第14~20行,使用了两个catch块,来捕捉算术运算异常和数组越界异常,并使用异常对象的printStackTrace()将对异常的堆栈跟踪信息全部显示出来,这对调试程序非常有帮助,也是常见的编程语言的集成开发环境(如Eclipse等)常用的手段。

一开始,我们将导致异常的两行语句注释起来(第10~11行),这样程序运行起来就没有任何问题,运行结果如上图所示。但是我们也可看到,即使没有任何异常,finally{}块内的语句还是照样运行了,其实这并非是必需的模块,这就告诉我们,要有取舍地决定是否使用finally{}块。

如果我们取消第10行开始处的单行注释符号“//”,然后重新运行这个程序,其运行结果如下所示。
Java—异常的捕获与处理_第12张图片
行结果表明,如果令arr[1] = 0,那么第12行就会产生“除数为0”的异常,但即使出现了异常,从第23行输出结果可以看到,程序仍能正常全部运行。如果没有异常处理程序,程序运行到第12行,就会终止,第12行前运行的中间结果就不得不全部抛弃(如果读者把12行想象成120行、1200行…就更能理解在某些情况下,这种被迫放弃中间计算结果可能是一种浪费)。

如果我们取消第11行开始处的单行注释符号“//”,然后重新运行这个程序,其运行结果如下所示。
Java—异常的捕获与处理_第13张图片
运行结果表明,如果令arr[10] = 7,10超过了数组的下标上限(4),因此也发生异常,但程序也能正确运行完毕。由此,我们可以看到,范例程序使用了多个catch,根据不同的异常分类,有的放矢地处理它们。

异常处理机制小结

当异常发生时,通常可用两种方法来处理,一种是交由Java默认的异常处理机制做处理。但这种处理方式,Java通常只能输出异常信息,接着便终止程序的运行,接着结束TestException的运行。

另一种处理方式是用自行编写的try-catch-finally块来捕捉异常,如DealException与EObject。自行编写程序代码来捕捉异常最大的好处是,可以灵活操控程序的流程,且可做出最适当的处理。下图绘出了异常处理机制的选择流程。
Java—异常的捕获与处理_第14张图片

异常类的处理流程

异常可分为两大类:java.lang.Exception类与java.lang.Error类。这两个类均继承自java.lang. Throwable类。下图为Throwable类的继承关系图。
Java—异常的捕获与处理_第15张图片
习惯上将Error类与Exception类统称为异常类,但两者本质上还是不同的。Error类通常指的是Java虚拟机(JVM)出错,用户无法在程序里处理这种错误。

不同于Error类,Exception类包含了一般性的异常,这些异常通常在捕捉到之后便可做妥善的处理,以确保程序继续运行。如【范例 异常处理的使用(DealException.java】所示的“ArrayIndexOutOfBoundsException”就是属于这种异常。在日后进行异常处理的操作之中,默认是针对于Exception进行处理,而对于Error而言,无需普通用户关注。为了更好地说明Java之中异常处理的操作特点,下面给出异常处理的流程。

⑴ 如果程序之中产生了异常,那么会自动地由JVM根据异常的类型,实例化一个指定异常类的对象;如果这个时候程序之中没有任何的异常处理操作,则这个异常类的实例化对象将交给JVM进行处理,而JVM的默认处理方式就是进行异常信息的输出,而后中断程序执行。

⑵ 如果程序之中存在了异常处理,则会由try语句捕获产生的异常类对象;然后将该对象与try之后的catch进行匹配,如果匹配成功,则使用指定的catch进行处理,如果没有匹配成功,则向后面的catch继续匹配,如果没有任何的catch匹配成功,则这个时候将交给JVM执行默认处理。

⑶ 不管是否有异常都会执行finally程序,如果此时没有异常,执行完finally,则会继续执行程序之中的其他代码,如果此时有异常没有能够处理(没有一个catch可以满足),那么也会执行finally,但是执行完finally之后,将默认交给JVM进行异常的信息输出,并且程序中断。

throws关键字

在Java标准库中的方法通常并没有处理异常,而是交由使用者来处理,如判断整数数据格式是否合法的Integer.parseInt()方法就会抛出NumberFormatException异常。这是怎么做到的?看一下API文档中的方法原型。
在这里插入图片描述
就是这个“throws”关键字,如果字符串s中没有包含可解析的整数就会“抛出”异常。使用throws声明的方法表示此方法不处理异常,而由系统自动将所捕获的异常信息“抛给”上级调用方法。throws使用格式如下。
Java—异常的捕获与处理_第16张图片
上面的格式包括两个部分:一个普通方法的定义,这和以前学习到的方法定义模式没有任何区别;方法后紧跟“throws 异常类”,它位于方法体{}之前,用来检测当前方法是否有异常,若有,则将该异常提交给直接使用这个方法的方法。

关键字throws的使用(throwsDemo.java)。
Java—异常的捕获与处理_第17张图片
Java—异常的捕获与处理_第18张图片
在第15~20行,定义了私有化的静态方法setZero(),用于将指定的数组的指定索引赋值为0,由于没有检查下标是否越界(当然,并不建议这样做,这里只是显示一个抛出方法异常的例子),所以使用throws关键字抛出异常,“ArrayIndexOutOfBoundsException”表明setZero()方法可能存在的异常类型。一旦方法出现异常,setZero()方法自己并不处理,而是将异常提交给它的上级调用者main()方法。而在main方法中,有一套完善的try-catch机制来处理异常。第07行中,调用setZero()方法,并有意使下标越界,用来验证异常检测与处理模块的运行情况。

throw关键字

到现在为止的所有异常类对象全部都是由JVM自动实例化的,但有时用户希望能亲自进行异常类对象的实例化操作,自己手工抛出异常,那么此时就需依靠throw关键字来完成了。

与throws不同的是,可直接使用throw抛出一个异常,抛出时直接抛出异常类的实例化对象即可。throw语句的格式为:
在这里插入图片描述
执行这条语句时,将会“引发”一个指定类型的异常,也就是抛出异常。

关键字throw的使用(throwDemo.java)。
Java—异常的捕获与处理_第19张图片
Java—异常的捕获与处理_第20张图片
第07行,创建一个匿名的ArrayIndexOutOfBoundsException类型的异常对象,并使用throw关键字抛出。引发运行期异常,用户可以给出自己个性化的提示信息。

第09~12行,捕获产生的异常对象,并输出异常信息。

或许大家有疑问,我们为什么要“没事找事”呢?这里首先要说明的是throw关键字的使用完全符合异常的处理机制,但是,一般来讲用户都在避免异常的产生,所以不会手工抛出一个新的异常类型的实例,而往往会抛出程序中已经产生的异常类实例。这点可以从下面的异常处理的标准格式中清晰发现。

异常处理的标准格式

学到现在,大家或许觉得throw关键字用途不大。那么这些技术到底该如何应用呢?实际上,try…catch…finally、throw及throws经常联合使用。例如,现在要设计一个将数组指定下标的元素置零的方法,同时要求在方法的开始和结束出都要输出相应信息。

关键字throws与throw的配合使用(throwDemo02.java)。
Java—异常的捕获与处理_第21张图片
Java—异常的捕获与处理_第22张图片
Java—异常的捕获与处理_第23张图片
第16~30行定义了私有化的静态方法setZero(),定义为静态方法的原因在于,这个方法可以不用生成对象即可调用,这样是为了简化代码,更清楚地说明当前问题。在第17行使用throws关键字,将setZero()方法中的异常传递给它的调用者main()方法中。

第25行使用throw抛出异常。throw总是出现在方法体中,一旦它抛出一个异常,程序会在throw语句后立即终止,后面的语句就没有机会执行,然后在包含它的所有try块中(可能在上层调用方法中)从里向外寻找含有与其异常类型匹配的catch块,然后加以处理。

第19行和第28行分别输出了方法开始和方法结束,当然这里只是给出一个示例,以后大家学到文件、数据库编程等时,可以在第28行的位置改成关闭文件或数据库连接。

RuntimeException类

在Java面试中经常会询问Exception类与RuntimeException类的区别,如果想理解这两个类的区别,请看以下一段代码。
Java—异常的捕获与处理_第24张图片
上面这段代码,一般情况下运行起来没有问题,代码中没有任何有关异常检测的语句。现在来观察一下Integer类之中的parseInt()方法定义。
在这里插入图片描述
通过查看parseInt()方法的原型,就可以发现,parseInt()使用了throws抛出一个异常,按照之前所学,现在应该强制性地使用try…catch语句块处理才对,可是程序并没有这样的强制性要求。在API中查一下NumberFormatException类的继承体系。
Java—异常的捕获与处理_第25张图片
可以发现NumberFormatException类继承自RuntimeEXception类,而在Java中明确规定对于RuntimeException的异常类型可以有选择性地来进行处理,如果不处理则出现异常时将交给JVM默认处理。为什么会这样呢?试想一下,在程序中经常会用到除法等操作,而除法有可能产生ArithmeticException类型的异常,如果RuntimeException类型的异常必须处理,可以想象那程序将变成什么样了?

常 见 的RuntimeException类 型 的 异 常 有: NumberFormatException、ClassCastException、NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException。

提示
请解释Exception和RuntimeException的区别?
· Exception:强制性要求用户必须处理;
· RuntimeException:是Exception的子类,由用户选择是否进行处理。

编写自己的异常类

在Java中本身已经提供了大量的异常类型,但是在开发之中,这些异常类型可能会不能满足于全部的开发需求,特别是在做一些软件的架构设计的时候,这些异常类可能不够用户去使用。为了处理各种异常,Java可通过继承的方式运行用户编写自己的异常类。因为所有可处理的异常类均继承自Exception类,因此自定义异常类也不例外。自定义编写异常类的语法如下。
Java—异常的捕获与处理_第26张图片
大家可以在自定义异常类里编写方法来处理相关的事件,甚至不编写任何语句也可以正常地工作,这是因为父类Exception已提供相当丰富的方法,通过继承子类均可使用它们。

定义自己的异常类(代码userDefinedException.java)
Java—异常的捕获与处理_第27张图片
Java—异常的捕获与处理_第28张图片
第14~20行声明了一个MyException类,此类继承自Exception类,所以此类为自定义异常类。第18行调用super关键字,调用父类(Exception)的一个参数的构造方法,传入的为异常信息。

Exception构造方法如下。
在这里插入图片描述
第6行用throw抛出一个MyException异常类的实例化对象。

在JDK中提供的大量API方法之中含有大量的异常类,但这些类在实际开发中往往并不能完全满足设计者对程序异常处理的需要,在这个时候就需要用户自己去定义所需的异常类,用一个类清楚地写出所需要处理的异常。

1. 异常类型的继承关系

异常类型的最大父类是Throwable类,其分为两个子类,分别为Exception、Error。Exception表示程序可处理的异常,而Error表示JVM错误,一般无需程序开发人员自己处理。

2. RuntimeException和Exception的区别

RuntimeException类 是Exception类 的 子 类,Exception定 义 了 必 须 处 理 的异 常,而RuntimeException定义的异常可以选择性地进行处理。

你可能感兴趣的:(Java)