第五章、异常、断言和日志

为了尽量避免程序出现错误,至少应该做到以下几点:

  • 向用户通知错误;
  • 保存所有的工作;
  • 允许用户妥善地退出程序。

一、处理错误

为了能够处理程序中的异常情况,必须考虑到程序中可能出现的错误和问题。那么需要考虑哪些问题呢?

  • 用户输入错误。除了那些不可避免的键盘输入错误外,有些用户喜欢自行其他,而不遵守程序的要求。例如,假设有一个用户情趣链接一个 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 子句中指定的一个异常类,那么

  1. 程序将跳过 try 语句块的其他代码。
  2. 程序将执行 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. 异常处理不能代替简单的测试。
  2. 不要过分地细化异常。
  3. 充分利用异常层次结构。
  4. 不要压制异常。
  5. 在检测错误时,“苛刻”要比放任更好。
  6. 不要羞于传递异常。

四、使用断言

在一个具有自我保护能力地程序中,断言很常用。

1、断言的概念

断言机制允许在测试期间向代码中插入一些检查,而在生产代码中会自动删除这些检查。

Java 语言引入了关键字 assert。这个关键字有两种形式:assert conditionassert 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

有些类不是由类加载器加载,而是直接由虚拟机加载的。可以使用这些开关有选择地启用或禁用哪些类中的断言。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一支帆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值