在Java中,如果某个方法不能采用正常的途径完成它的任务,可以通过另一个途径退出方法。在这种情况下,方法并不会返回任何值,而是抛出(throw)了一个封装了错误信息的对象。需要注意的是,这个方法会立刻退出,并不返回正常值(或任何值)。此外,也不会从调用这个方法的代码继续执行,取而代之的是,异常处理机制开始搜索能够处理这种异常状况的异常处理器。
异常有自己的语法和特定的继承层次结构。下面我们先介绍语法,然后再给出有效地使用这种语言特性地技巧。
一、异常的分类
在Java语言中,异常对象都是派生于Throwable类的一个类实例。
我们来看一个大概的简化图:
可以看出,所有异常都是由Throwable继承而来,但在下一层立即分解为两个分支:Error和Exception
1> Error类
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。这种错误是java虚拟机jvm报出的。如果出现了这种内部错误,无法解决,只能终止程序。
2> Exception类
在设计Java程序时,要重点关注Exception层次结构。
2.1 RunTimeException
派生于RunTimeException:一般是由编译错误导致的异常(但是编译可以通过,既不用捕获也不用抛出,只是在运行时程序检测出,然后停止),常包括以下问题:
- 错误的强制类型转换
- 数组访问越界
- 访问null指针
如果出现RunTimeException异常,那么一定就是你的问题
2.2 其他异常
不是派生于RunTimeException的异常属于其他异常,常包括以下问题:
- 试图超越文件末尾来读取文件
- 试图打开一个不存在的文件
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类并存在
2.3 检查型异常与非检查型异常
Java语言将派生于Error类或RunTimeException类的所有异常称为非检查型异常,所有的其他的异常称为检查型异常。
编译器将检查你是否为所有的检查型异常提供了异常处理器
二 、声明检查型异常
1> 声明异常的规则
如果遇到了无法处理的情况,Java方法可以跑出一个异常。这个道理很简单:方法不仅需要告诉编译器将要返回什么值, 还要告诉编译器有可能发生什么错误。
要在方法的首部指出这个方法可能抛出一个异常,所以要修改方法的首部,以反映这个方法可能抛出的检查型异常,例如:
public
当然也可以声明多个异常,例如:
class
2> 继承关系中抛异常的规则
- 如果子类中重写了父类的中的一个方法,子类中声明的检查型异常不能比父类中声明的异常更通用(即,子类中声明的异常不能时父类中声明的异常的父类),由此可得,如果父类的方法没有声明异常,那么子类重写这个方法时,不能声明任何异常。
自己编写方法时,不必声明这个方法可能抛出的所有异常。至于什么时候需要在方法中使用throws子句声明异常,以及声明出哪些异常,我们需要记住下面的几点:
- 调用了一个抛出检查型异常的方法。(一般,编译器如IDEA,eclipse等编译器)
- 检测到一个错误,并利用throw语句抛出一个检查型异常
- 如果类中的一个方法声明它会抛出一个异常,而这个异常是某个特定类的实例,那么这个方法抛出的异常可能属于这个类,也可能属于这个类的任意一个子类。
三、如何抛出异常
现在我们假设一个名为readDate( )的方法正在读取一个文件,文件首部包含以下信息,承诺文件长度为1024哥字符。(这里使用的例子是有关IO流的内容,不懂得可以先可以自己查API文档)
然而,读到了733个字符之后文件就结束了。这时候并不是一个正常的情况,需要抛出一个异常。阅读API文档以后,我们发现EOFException很合适,它的描述是:指示输入过程中意外遇到了EOF。
来看示例代码:
String
我们来总结一下:
- 抛出异常的标准规范语言:用关键词throw
throw
- 如果一个已有的异常类能够满足你的要求,抛出这个异常非常容易。
在这个情况下:
- 找到一个合适的异常类
- 创建这个类的一个对象
- 将对象抛出
四、创建异常类与描述异常
1> 创建异常类
有时候我们可能会遇到任何标准异常类都无法描述的问题。这种情况下,我们就需要创建自己的异常类了。
以下为创建异常类的标准步骤:
- 自定义一个继承于Exception或者Exception任何子类的一个类
- 创建一个默认的构造器
- 创建一个包含描述信息的构造器(超类Throwable的toString( )方法会返回一个字符串,其中包含这个详细信息,这在调试中非常有用)
以下为示例代码:
class
2> 描述异常
我们来看示例代码:
String
其中:
String
就是在描述这个异常的信息
五、捕获异常
1> 捕获异常
5.1.1 trt/catch语句块
如果发生了某个异常,但没有在任何地方捕获这个异常,程序就会终止。所以我们就需要捕获异常。
要想捕获异常,我们就要使用try/catch语句块,如下所示:
try
当然我们也可以捕获多个异常:
try
- 如果try语句块中的任何代码抛出了catch子句中指定的一个异常类,那么
- 程序将跳过try语句块的其余代码
- 程序将执行catch子句中的处理器代码
- 如果try语句中的代码没有抛出任何异常,那么程序将跳过catch子句
- 如果方法中的任何代码抛出了catch子句中声明的一个异常类型,那么这个方法就会立刻退出。
5.1.2 抛异常与捕获异常
抛异常:即前面提的声明异常:throws
捕获异常:try/catch
那该什么时候抛异常,什么时候捕获异常呢?
请记住:
如果你知道该怎样处理这个异常,就去捕获他。如果你不知道怎样该处理这个异常,那就去先抛出他,让他的调用者来捕获
我们也得记住,最后在main( )方法中,我们必须捕获抛出的异常,如果继续抛异常,那么就会把这个异常交给虚拟机,就相当于了Error
3> 再次抛出异常与异常链(了解)
可以在catch子句中抛出一个异常。通常,希望改变异常的类型时会这样做。
这样很有道理,如果开发了一个供其他程序员使用的子系统,可以使用一个指示子系统故障的异常类型。
来看示例代码:
try
我们也可以把原始异常设置为新异常的“原因”:
try
捕获到这个异常时,可以使用下面的这条语句获取原始异常:
Throwable
4> finally 子句
代码在抛出一个异常时,就会停止处理这个方法剩余的代码,并退出这个方法。
但是,如果这个方法已经获得了只有它自己知道的一些本地资源,而且这些资源必须清理,这就会有问题。
finally子句可以解决这些问题:不管是否有异常被捕获,finally子句中的代码都会被执行。
我们来分析一个代码
如下所示:
var
- 当代码没有抛出异常。在这种情况下,程序首先执行try语句块中的全部代码,然后执行finally子句中的代码。随后,继续执行finally子句之后的第一条语句。也就是说,执行的顺序是 1、2、5、6
- 代码抛出一个异常,并在一个catch子句中被捕获。在这种情况下,程序将执行try语句中的所有代码,直到抛出异常为止。此时,将跳过try语句中的剩余代码,转去执行与该异常匹配的catch子句中的代码,最后执行finally中的子句。
- 如果catch子句没有抛出异常,程序将执行finally子句之后的第一条子句。这种情况下执行的顺序是:1、3、4、5、6
- 如果catch子句抛出了一个异常,异常将被炮灰这个方法的调用者。执行的顺序只是:1、3、5
- 代码抛出了一个异常,但没有任何catch子句捕获这个异常。在这种情况下,程序将执行try语句中的所有语句,直到抛出异常为止。此时,将跳过try语句中的剩余代码,然后执行finally子句中的语句,并将异常抛回给这个方法的调用者。在这里执行的顺序是:1、5
5> try-with-Resources语句
try-with-Resources语句(带资源的try语句)的最简形式为:
try
try块退出时,会自动调用res.close( )
我们来看一个典型的例子:
这里要读取一个文件的所有单词
try
不论这个块如何退出,in和out都会关闭。就好像finally子句中调用了in 和 out 的关闭方法
如果try块抛出一个异常,而且close( )方法也抛出一个异常,这就会带来一个难题。try-with-Resources语句可以很好地处理这种情况。原来的异常会重新抛出,而close( )方法抛出的异常则会“被抑制”。这些异常将自动的捕获,并由addSuppressed( )方法,它会生成从close( )方法抛出,并被抑制的异常数组。