为了尽量避免程序出现错误,至少应该做到以下几点:
- 向用户通知错误;
- 保存所有的工作;
- 允许用户妥善地退出程序。
一、处理错误
为了能够处理程序中的异常情况,必须考虑到程序中可能出现的错误和问题。那么需要考虑哪些问题呢?
- 用户输入错误。除了那些不可避免的键盘输入错误外,有些用户喜欢自行其他,而不遵守程序的要求。例如,假设有一个用户情趣链接一个 URL,而这个 URL 语法却不正确。你的代码应该对此进行检查,如果没有检查,网络层就会报错。
- 设备错误。硬件并不总是让它做什么,它就做什么。打印机可能被关掉了。网页可能临时性地不能浏览。在任务的处理过程中,硬件经常会出现问题。例如,打印机在打印过程中可能没有纸了。
- 物理限制。硬盘已满,你可能已经用尽了所有可用的储存空间。
- 代码错误。程序方法有可能没有正确地完成工作。例如,方法可能返回了一个错误的答案,或者错误地调用了其他方法。计算一个无效的数组索引,试图在散列表中查找一个不存在的记录,或者试图让一个空栈执行弹出操作,这些都属于代码错误。
对于方法中的错误,传统的做法是返回一个特殊的错误码,由调用方法分析。例如,对于从文件中读取信息的方法,返回值通常不是标准字符,而是一个 -1,表示文件结束。
这对于处理很多异常状况都是很高效地方法。还有一种表示错误状况的常用返回值是 null 引用。
遗憾的是,并不是任何情况下都能够返回一个错误码。有可能无法明确地将有效数据与无效数据加以区分。一个返回整型地方法就不能简单地通过返回 -1 表示错误,因为 -1 很可能是一个完全合法地结果。
在 Java 中,如果某个方法不能够采用正常的途径完成它的任务,可以通过另外一个路径退出方法。在这种情况下,方法并不返回任何值,而是抛出(throw)一个封装了错误信息的对象。需要注意的是,这个方法将会立刻退出,并不返回正常值(或任何值)。
此外,也不会从调用这个方法地代码继续执行,取而代之的是,异常处理机制开始搜索能够处理这种异常状况的异常处理器。
1、异常分类
在 Java 程序设计语言中,异常对象都是派生于 Throwable 类的一个类实例。
下图是 Java 异常层次结构的一个简化示意图:
需要注意的是,所有的异常都是由 Throwable 继承而来,但在下一层立即分解为两个分支:Error 和 Exception。
Error 类层次结构描述了 Java 运行系统的内部错误和资源耗尽错误。你的应用程序不应该抛出这种类型的对象。如果出现了这样的内部错误,除了通知用户,并尽力妥善地种植程序之外,你几乎无能为力。这种情况很少出现。
在设计 Java 程序时,要重点关注 Exception 层次结构。这个层次结构又分解为两个分支:一个分支派生于 RuntimeException;另一个分支包含其他异常。一般规则是:由编程错误导致的异常属于 RuntimeException;如果程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于其他异常。
派生于 RuntimeException 的异常包括以下问题:
- 错误的强制类型转换。
- 数组访问越界。
- 访问 null 指针。
不是派生于 RuntimeException 的异常包括:
- 试图超越文件末尾继续读取数据。
- 试图打开一个不存在的文件。
- 试图根据给定的字符串查找 Class 对象,而这个字符串表示的类并不存在。
Java 语言规范将派生于 Error 类或 RuntimeException 类的所有异常称为非检查型(unchecked)异常,其他所有的异常称为检查型(checked)异常。这是很有用的术语,在后面还会用到。编译器将检查你是否为所有的检查型异常提供了异常处理器。
2、声明检查型异常
要在方法的首部指出这个方法可能抛出一个异常,所以要修改方法首部,以反映这个方法可能抛出的检查型异常。例如,下面是标准类库中 FileInputStream 类的一个构造器的声明:
public FileInputStream(String name) throws FileNotFoundException
这个方法表示这个构造器将根据给定的 String 参数产生一个 FileInputStream 对象,但也有可能出错而抛出一个 FileNotFoundException 异常。如果发生了这种糟糕情况,构造器将不会初始化一个新的 FileInputStream 对象,而是抛出一个 FileNotFoundException 类对象。如果这个方法真的抛出了这样一个异常对象,运行时系统就会开始搜索知道如何处理 FileNotFoundException 对象的异常处理器。
3、如何抛出异常
假设一个名为 readData 的方法正在读取一个文件,然而,在读到 722 个字符之后文件就结束了。这时我们需要抛出一个异常。
首先要决定应该抛出什么类型的异常。可能某种 IOException 是个不错的选择。仔细阅读 Java API 文档之后会发现:EOFException 异常的描述是:“指示输入过程中意外遇到了 EOF”。完美,这正是我们要抛出的异常。下面是抛出这个异常的语句:
throw new EOFException();
或者,也可以是
var e = new EOFException();
throw e;
下面将这些代码放在一起:
String readData(Scanner in) throws EOFException{
...
while (...) {
if (!in.hasNext()) { // EOF encountered
if (n < len) {
throw new EOFException();
}
...
}
}
...
}
EOFException 类还有一个字符串参数的构造器。可以用此很好地描述异常:
String gripe = "Content - length: " + len + ", Received: " + n;
throw new EOFException(gripe);
4、创建异常类
在真实开发过程中,可能会遇到任何标准异常类都无法描述清楚的问题。在这种情况下,可以创建自己的异常类。我们需要做的只是定义一个派生于 Exception 的类,或者派生于 Exception 的某个子类,如 IOException。习惯的做法是,自定义的这个类应该包含两个构造器,一个是默认的构造器,另一个是包含详细描述信息的构造器。
class FileFormatException extends IOException{
public FileFormatException() {}
public FileFormatException(String gripe) {
super(gripe);
}
}
现在,就可以抛出自己定义的异常类型了。
String readData(BufferedReader in) throws FileFormatException{
...
while (...) {
if (ch == -1) { //EOF encountered
if (n < len) {
throw new FileFormatException();
}
}
...
}
return s;
}
java.lang.Throwable
- Throwable()
构造一个新的 Throwable 对象,但没有详细的描述信息。- Throwable(String message)
构造一个新的 Throwable 对象,带有指定的详细描述信息。- String getMessage()
获得 Throwable 对象的详细描述信息。
二、捕获异常
抛出异常,十分容易,只要将其抛出就不必理睬了。当然,有些代码必须捕获异常。捕获异常需要做更多规划。
1、捕获异常
要想捕获一个异常,需要设置 try/catch 语句块。最简单的 try 语句块如下所示:
try {
...
} catch (ExceptionType e) {
...
}
如果 try 语句块中的任何代码抛出了 catch 子句中指定的一个异常类,那么
- 程序将跳过 try 语句块的其他代码。
- 程序将执行 catch 子句中的处理器代码。
如果 try 语句块中的代码没有抛出任何异常,那么程序将跳过 catch 子句。
2、捕获多个异常
在一个 try 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。腰围每个异常类型使用一个单独的 catch 子句,如下例所示:
try {
// 业务代码
} catch (FileNoFoundException e) {
// 异常处理
} catch (UnknownMostException e) {
// 异常处理
} catch (IOException e) {
// 异常处理
}
要想获得这个对象的更多信息,可以尝试使用 e.getMessage()
3、finally 子句
不管是否有异常捕获,finally 子句中的代码都会执行。在下面的示例中,所有情况下程序都将关闭输入流。
var in = new FileInputStream(...);
try {
// 业务代码
} catch (IOException e) {
...
} finally {
in.close();
}
三、使用异常的技巧
- 异常处理不能代替简单的测试。
- 不要过分地细化异常。
- 充分利用异常层次结构。
- 不要压制异常。
- 在检测错误时,“苛刻”要比放任更好。
- 不要羞于传递异常。
四、使用断言
在一个具有自我保护能力地程序中,断言很常用。
1、断言的概念
断言机制允许在测试期间向代码中插入一些检查,而在生产代码中会自动删除这些检查。
Java 语言引入了关键字 assert。这个关键字有两种形式:assert condition
和 assert condition : expression
这两个语句都会计算条件,如果结果为 false,则抛出一个 AssertionError 异常。在第二个语句中,表达式将传入 AssertionError 对象的构造器,并转换成一个消息字符串。
要想断言 x 是一个非负数,只需要简单地使用下面这条语句
assert x >= 0;
或者将 x 地实际值传递给 AssertionError 对象,以便以后显示
assert x >= 0 : x;
2、启用和禁用断言
在默认情况下,断言是禁用的。可以在运行程序时用 -enableassertions 或 -ea 选项启用断言:
java -enableassertion MyApp
需要注意的是,不必重新编译程序来启用或禁用断言。启用或禁用断言时类加载器(class loader)的功能。禁用断言时,类加载器会去除断言代码,因此,不会降低程序运行的速度。
也可以在某个类或整个包中启用断言,例如:
java -ea:MyClass -ea:com.mycompany.mylib MyApp
这条命令将为 MyClass 类以及 com.mycompany.mylib 包和它的子包中的所有类打开断言。选项 -ea 将打开无名包中所有类的断言。
也可以用选项 -disableassertions 或 -da 在某个特定类和包中禁用断言:
java -ea:... -da:MyClass MyApp
有些类不是由类加载器加载,而是直接由虚拟机加载的。可以使用这些开关有选择地启用或禁用哪些类中的断言。