第十二章 异常处理和文本I/O
12.1引言
异常是运行时错误。异常处理使得程序可以处理运行时错误,并且继续通常的执行。
在程序运行过程中,如果JVM检测出一个不可能执行的操作,就会出现运行时错误(runtime error)。
在Java中,运行时错误会作为异常抛出。异常就是一种对象,表示阻止正常运行的错误或者情况。
12.2 异常处理概述
异常是从方法抛出的。方法的调用者可以捕获以及处理该异常。
下面请看如下例子,该例子表明读取两个整数并显示它们的商。
如果输入0赋值给第二个数字,那就会产生一个运行时错误,因为不能用一个整数除以0(注意,一个浮点数除以0是不会产生异常的)。解决这个错误的一个简单方法就是添加一个if语句来测试第二个数字,如下图所示。
下面重写上面的代码,使用一个方法计算商,如下图所示。
方法 quotient
返回两个整数的商。如果number2
为0,则不能返回一个值,因此程序在第7行终止。这显然是一个问题。不应该让方法来终止程序——应该由调用者决定是否终止程序。
方法如何通知它的调用者一个异常产生了呢?Java可以让一个方法可以抛出一个异常该异常可以被调用者捕获和处理。上面所示代码可以重写如下图所示。
如果number2为0,方法通过执行以下语句抛出一个异常(第6行):
throw new ArithmeticException("Divisor cannot be zero");
在这种情况下,抛出的值为new ArithmeticException("Divisor cannot be zero")
,称为一个异常(exception)。throw
语句的执行称为抛出一个异常(throwing an exception)。异常就是一个从异常类创建的对象。在这种情况下,异常类就是java.lang.ArithmeticException
。构造方法 ArithmeticException(str)
被调用以构建一个异常对象,其中str
是描述异常的消息。
当异常被抛出时,正常的执行流程被中断。就像它的名字所提示的,“抛出异常”就是将异常从一个地方传递到另一个地方。调用方法的语句包含在一个try
块和一个catch
块中。try
块(第19~23行)包含了正常情况下执行的代码。异常被catch
块所捕获。catch
块中的代码被执行以处理异常。之后,catch
块之后的语句(第29行)被执行。throw
语句类似于方法的调用,但不同于调用方法的是,它调用的是catch
块。从某种意义上讲,catch
块就像带参数的方法定义,这些参数匹配抛出的值的类型。但是,它不像方法,在执行完catch
块之后,程序控制不返回到throw
语句;而是执行catch
块后的下一条语句。
catch 块的头部
catch(ArithmeticException ex)
中的标识符ex
的作用很像是方法中的参数。所以,这个参数称为catch
块的参数。ex
之前的类型(例如,ArithmeticException)指定了catch
块可以捕获的异常类型。一旦捕获该异常,就能从catch
块体中的参数访问这个抛出的值。总之,一个try-throw-catch
块的模板可能如下所示:
try {
Code to run;
A statement or a method that may throw an exception;
More code to run;
}
catch(type ex){
Code to process the exception;
}
一个异常可能是通过try块中的throw语句直接抛出,或者调用一个可能会抛出异常的方法而抛出。
12.3 异常类型
异常是对象,而对象都采用类来定义。异常的根类是java.lang.Throwable
。
下图是一些常见的异常类
注意:类名Error
、Exception
和RuntimeException
有时候容易引起混淆。这三种类都是异常,这里讨论的错误都发生在运行时。
Throwable
类是所有异常类的根。所有的Java
异常类都直接或者间接地继承自Throwable
。可以通过继承Exception
或者Exception
的子类来创建自己的异常类。
这些异常类可以分为三种主要类型:系统错误、异常和运行时异常。
- 系统错误(system error)是由
Java
虚拟机抛出的,用Error
类表示。Error
类描述的是内部系统错误。这样的错误很少发生。如果发生,除了通知用户以及尽量稳妥地终止程序外,几乎什么也不能做。下图列出了Error
的子类的一些例子。
-
异常(exception)是用
Exception
类表示的,它描述的是由你的程序和外部环境所引起的错误,这些错误能被程序捕获和处理。下图列出Exception
类的子类的一些例子。 -
运行时异常(runtime exception)是用
RuntimeException
类表示的,它描述的是程序设计错误,例如,错误的类型转换、访问一个越界数组或数值错误。运行时异常通常表明了编程错误。下图列出RuntimeException
的子类的一些例子。
12.4 声明、抛出和捕获异常
异常的处理器是通过从当前的方法开始,沿着方法调用链,按照异常的反向传播方向找到的。
Java的异常处理模型基于三种操作:声明一个异常(declaring an exception)、抛出一个异常(throwing an exception)和捕获一个异常(catching an exception),如下图所示。
12.4.1 声明异常
在Java中,当前执行的语句必属于某个方法。Java
解释器调用main
方法开始执行一个程序。每个方法都必须声明它可能抛出的必检异常的类型。这称为声明异常(declaring exception)。
为了在方法中声明一个异常,就要在方法头中使用关键字throws
,如下所示:
public void myMethod() throws IOException
关键字 throws
表明 myMethod
方法可能会抛出异常IOException
。如果方法可能会抛出多个异常,就可以在关键字throws
后添加一个用逗号分隔的异常列表:
public void myMethod()
throws Exception1,Exception2,...,ExceptionN
如果父类中的方法没有声明异常,那么就不能在子类中对其重写时声明异常。
12.4.2 抛出并常
检测到错误的程序可以创建一个合适的异常类型的实例并抛出它,这称为抛出一个异常(throwing an exception)。这里有一个例子,假如程序发现传递给方法的参数与方法的合约不符(例如,方法中的参数必须是非负的,但是传入的是一个负参数),这个程序就可以创建IllegalArgumentException
的一个实例并抛出它,如下所示:
IllegalArgumentException ex =
new I11egalArgumentException("Wrong Argument");
throw ex;
或者,根据偏好,也可以使用下面的语句:
throw new IllegalArgumentException("Wrong Argument");
注意:IllegalArgumentException
是JavaAPI
中的一个异常类。通常,Java API
中的每个异常类至少有两个构造方法:一个无参构造方法和一个带有可以描述这个异常的String
参数的构造方法。该参数称为异常消息(exception message),它可以通过一个异常对象调用 getMessage()
获取。
提示:声明异常的关键字是throws
,抛出异常的关键字是throw
。
12.4.3 捕获异常
现在我们知道了如何声明一个异常以及如何抛出一个异常。当抛出一个异常时,可以在try-catch
块中捕获和处理它,如下所示:
try {
statements; //Statements that may throw exceptions
} catch(Exception1 exVar1) {
handler for exception1;
} catch(Exception2 exVar2) {
handler for exception2;
}
...
catch(ExceptionN exVarN) {
handler for exceptionN;
}
如果在执行try
块的过程中没有出现异常,则跳过catch
子句。
如果try
块中的某条语句抛出一个异常,Java 就会跳过 try
块中剩余的语句,然后开始查找处理这个异常的代码。处理这个异常的代码称为异常处理器(exception handler);可以从当前的方法开始,沿着方法调用链,按照异常的反向传播方向找到这个处理器。从第一个到最后一个逐个检查catch
块,判断在catch
块中的异常类实例是否是该异常对象的类型。如果是,就将该异常对象赋值给所声明的变量,然后执行catch
块中的代码。如果没有发现异常处理器,Java会退出这个方法,把异常传递给这个方法的调用者,继续同样的过程来查找处理器。如果在调用的方法链中找不到处理器,程序就会终止并且在控制台上打印出错信息。查找处理器的过程称为捕获一个异常(catching an exception)。
抛出一个异常,如下图所示。考虑下面的情形:
假设main
方法调用method1,method1
调用method2,method2
调用 method3,method3
抛出一个异常,如下图所示。考虑下面情形:
如果异常类型是Exception3
,它就会被method2
中处理异常ex3
的catch
块捕获。跳过statement5
,然后执行statement6
。
如果异常类型是Exception2
,则退出method2
,控制被返回给method1
,而这个异常就会被method1
中处理异常ex2
的catch
块捕获。跳过statement3
,然后执行statement4
。
如果异常类型是Exception1
,则退出method1
,控制被返回给main
方法,而这个异常就会被main
方法中处理异常ex1
的catch
块捕获。跳过statement1
,然后执行statement2
。
如果异常类型没有在method2
、method1
和main
方法中被捕获,程序就会终止。不执行statement1
利用statement2
。
注意:各种异常类可以从一个共同的父类中派生。如果一个catch
块可以捕获一个父类的异常对象,它就能捕获那个父类的所有子类的异常对象。
12.4.4 从异常中获取信息
异常对象中包含关于异常的有价值的信息。可以利用下面这些java.lang.Throwable
类中的实例方法获取有关异常的信息,如下图所示。printstackTrace()
方法在控制台上打印栈的跟踪信息。栈的跟踪列出调用栈中所有的方法,这为调试运行时错误提供了很有用的信息。getstackTrace()
方法提供编程的方式,来访问由printstackTrace()
打印输出的栈跟踪信息。
12.5 finally子句
无论异常是否产生,finally子句总会被执行。
有时候,不论异常是否出现或者是否被捕获,都希望执行某些代码。Java的finally
子句可以用来实现这个目的。finally
子句的语法如下所示:
try {
statements;
} catch (TheException ex) {
handling ex;
}
finally {
finalStatements;javajvav
}
在任何情况下,finally
块中的代码都会执行,不论try
块中是否出现异常或者是否被捕获。考虑下面三种可能出现的情况:
- 如果
try
块中没有出现异常,执行finalStatements
,然后执行try
语句的下一条语句。 - 如果
try
块中有一条语句引起了异常并被catch
块捕获,会跳过try
块的其他语句执行catch
块和finally
子句。执行try
语句后的下一条语句。 - 如果
try
块中的一条语句引起异常,但是没有被任何catch
块捕获,就会跳过try
块中的其他语句,执行finally
子句,并且将异常传递给这个方法的调用者。
即使在到达 finally
块之前有一个return
语句,finally
块还是会执行。
注意:使用finally
子句时可以略去catch
块。
12.6 何时使用异常
当错误需要被方法的调用者处理的时候,方法应该抛出一个异常。
try
块包含正常情况下执行的代码。catch
块包含异常情况下执行的代码。异常处理将错误处理代码从正常的编程任务中分离出来,这样,可以使程序更易读、更易修改。但是应该注意,由于异常处理需要初始化新的异常对象,需要从调用栈返回,而且还需要沿着方法调用链来传播异常以便找到它的异常处理器,所以,异常处理通常需要更多的时间和资源。
12.7 重新抛出异常
如果异常处理器不能处理一个异常,或者只是简单地希望它的调用者注意到该异常,Java 允许该异常处理器重新抛出异常。
重新抛出异常的语法如下所示:
try {
statements;
}
catch (TheException ex) {
perform operations before exits;
throw ex;
}
语句throw ex
重新抛出异常给调用者,以便调用者的其他处理器获得处理异常ex
的机会。
12.8 链式异常
与另一个异常一起抛出一个异常,构成了链式异常。
有时候,可能需要同最初异常一起抛出一个新异常(带有附加信息),这称为链式并常(chained exception)。下图展示了如何产生和抛出链式异常。
12.9 创建自定义异常类
可以通过继承 java.lang.Exception
类来定义一个自定义异常类。
Java提供相当多的异常类,尽量使用它们而不要创建自己的异常类。然而,如果遇到一个不能用预定义异常类来充分描述的问题,那就可以通过继承Exception
类或其子类(例如,IOException)来创建自己的异常类。
12.10 Fie类
File类包含了获得一个文件/目录的属性,以及对文件/目录进行改名和删除的方法。
在学完异常处理后,我们来学习文件处理。存储在程序中的数据是暂时的,当程序终止时它们就会丢失。为了能够永久地保存程序中创建的数据,需要将它们存储到磁盘或其他永久存储设备的文件中。这样,这些文件之后可以被其他程序传输和读取。由于数据存储在文件中,所以本节就介绍如何使用File
类获取文件/目录的属性以及删除和重命名文件/目录,以及创建目录