避免使用空指针作为标志以防止出现异常
2008-01-05 08:40:54 来源:WEB开发网核心提示:避免使用空指针作为标志以防止出现异常 Eric E. Allen软件工程师,Cycorp, Inc.为开发健壮的程序,避免使用空指针作为标志以防止出现异常,我们经常用空指针代替异常情况,但这实际上却把控制流限制在方法调用和返回的普通方式,目前,他正在为 NextGen 编程语言实现一种从源代码到字节代码的编译器,同时也
避免使用空指针作为标志以防止出现异常
Eric E. Allen
软件工程师,Cycorp, Inc.
为开发健壮的程序,我们经常用空指针代替异常情况,但这实际上却把控制流限制在方法调用和返回的普通方式,同时也隐藏了异常情况发生的迹象。在这篇专栏里,Eric Allen 展示了这种错误模式(他称之为空标志错误模式)怎样产生难以调试的意外结果。和我们讨论过的其它错误模式一样,您可以应用某种编程技巧来减少这种错误的出现。
空标志错误模式
在我的上一篇文章中,我说明了用空指针代替各种不同基本类型的数据是如何成为引起 NullPointerException 异常最普遍的原因之一的。这一次,我将说明用空指针代替异常情况怎么也会导致问题的出现。在 java 程序中,异常情况通常是通过抛出异常,并在适当的控制点捕捉它们来进行处理。但是经常看到的方法是通过返回一个空指针值来表明这种情况(以及,可能打印一条消息到 System.err)。假如调用方法没有明确地检查空指针,它可能会尝试丢弃返回值并触发一个空指针异常。
您可能会猜想,之所以称这种模式为空标志错误模式,是因为它是不一致地使用空指针作为异常情况的标志引起的。
起因
让我们来考虑一下下面的这个简单的桥类(从 BufferedReaders 到 Iterators):
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.Iterator;
public class BufferedReaderIterator implements Iterator {
PRivate BufferedReader internal;
public BufferedReaderIterator(BufferedReader _internal) {
this.internal = _internal;
}
public boolean hasNext() {
try {
boolean result = true;
// Let′s suppose that lines in the underlying input stream are known
// to be no greater than 80 characters long.
internal.mark(80);
if (this.next() == null) {
result = false;
}
internal.reset();
return result;
}
catch (IOException e) {
System.err.println(e.toString());
return false;
}
}
public Object next() {
try {
return internal.readLine();
}
catch (IOException e) {
System.err.println(e.toString());
return null;
}
}
public void remove() {
// This iterator does not support the remove Operation.
throw new UnsupportedOperationException();
}
}
因为这个类作为 Iterator 接口的桥接实现,代码必须从 BufferedReader 捕捉 IOException 异常。每一种方法通过返回某个缺省值来处理 IOException。对于 hasNext,返回 false 值。这是合理的,因为假如 IOException 异常被抛出,客户就不应该指望能从 Iterator 检索到另一个元素。另一方面,在 IOException 异常(因为它取决于 internal.readLine() 的返回值)和 internal 是空的情况下,next 都返回 null。但这不是 Iterator 对象的客户所期待的。正常情况下,在没有更多元素的 Iterator 上调用 next 时,会抛出一个 NoSUChElementException 异常。假如我们的 Iterator 的客户依靠于这种行为,它很可能会尝试丢弃从调用 next 返回的空指针,结果导致 NullPointerException 异常。
不管 NullPointerException 异常什么时候出现,都要对如上所述的情况作检查。这种错误模式的出现很普遍。
预防措施
尽管这种错误模式经常出现,使用空标志仍是非常没有根据的(与上例的情况一样)。让我们来重写 next,使它如我们期望的一样抛出 NoSuchElementException 异常:
public Object next() {
try {
String result = internal.readLine();
if (result == null) {
throw new NoSuchElementException();
}
else {
return result;
}
}
catch (IOException e) {
// The original exception is included in the message to notify the
// client that an IOException has occurred.
throw new NoSuchElementException(e.toString());
}
}
请注重:要使其余的代码能使用修改过的方法,我们还必须:
导入 java.util.NoSuchElementException。
修正 hasNext,使其不再调用 next 来进行测试。最简单的修正方法是只要直接调用 internal.readLine()。
另一种处理 IOException 异常的方法是捕捉它们,并代替它们抛出 RuntimeException 异常。决定这样做是基于对目标平台上预计 IOException 异常出现频率的估计。假如很频繁,那么您可能想试着从中恢复。
调用这个新 next 方法的任何代码可能都不得不处理抛出的 NoSuchElementException 异常。(当然,代码可以简单地选择忽略它们并答应程序异常终止。)假如这样,与原始代码抛出的 NullPointerException 异常相比,产生的错误消息和抛出异常的位置所提供的信息要丰富得多。假如抛出的异常是检查过的异常(比如 IOException),那么它会更有用,因为除非处理了异常,否则类的客户代码将不编译。利用这种方法,我们甚至可以在程序运行前排除某些错误发生的可能性。但是,在这个示例中,不破坏 Iterator 接口,就不能抛出这样一个检查过的异常。因此,为了重复使用在 Iterators 上运行的代码,我们牺牲了一些静态检查。静态检查的目的和重复使用的目的之间的这种矛盾是很普遍的。
总结
在我完成这篇文章前,我要提醒许多经常使用空标志的程序员注重。许多程序员会争辩说这会使他们的程序更“健壮”。究竟,他们可能会说,健壮的系统能够适当地处理不同的情况,而不是一碰到小问题就抛出异常。但是这种争辩忽视了这样一种事实,即异常是增强代码健壮性的有力工具,它答应在异常情况下控制能快速传送到最适合控制的位置。另一方面,空标志的使用把控制流限制在方法调用和返回的普通方式(当然,一直到整个程序崩溃)。此外,这样使用空标志,程序员有效地掩盖了异常情况出现位置的迹象。谁知道空指针在被丢弃前从方法到方法传递了多远?这只能使得诊断错误以及确定怎样修正它们更加困难。经验证实这种代码经常中断。我们首要关注的应该是避免这种困惑,使诊断尽可能轻易。因此,作为准则,我努力编写可以尽快通知异常情况的代码,并且尝试着仅从没有指示程序错误的异常情况中恢复。
即使在代码中尽量避免使用空标志,您仍要不可避免地处理使用了空标志的旧代码。事实上,许多 Java 类库本身,比如我们上面使用过的 Hashtable 类和 BufferedReader 类都用了空标志。当使用这样的类时,您可以通过在执行前,显式检查操作是否将返回空来避免错误。例如,对于 Hashtables,我总是在调用 get 之前用 containsKey 进行测试。但是,尽管采用这种预防手段,这种错误模式仍然是最常碰到的错误模式之一。
下面是本周的错误模式的小结:
模式:空标志
症状:使用空指针作为异常情况的标志的代码块报告 NullPointerException 异常。
起因:调用方法没有检查作为返回值的空指针。
治疗和预防措施:抛出异常来报告异常情况。
在下一篇文章中,我将讨论与类强制转换异常有关的错误模式。
参考资料
请务必阅读 Eric Allen 的前一个关于错误模式的诊断 Java 代码专栏:
The Dangling Composite bug pattern(developerWorks,2001 年 3 月)
错误模式:介绍(developerWorks ,2001 年 2 月)
一种防止异常情况处理不一致的方法是面向表征的编程: 一种将程序的经常绕过模块边界的部分模块化的编程风格。请查看 aspectJ,Java 语言的一种面向表征的扩展,带有支持许多流行的 Java IDE 的实现。
静态确定可能出现空指针异常的方法是一种被称为 set-based analysis 的技术。The Carnegie Mellon School of Computer Science 网站为这种方法提供了简短介绍,同时还提供与本文相关的一些技术出版物的链接。
DePaul 大学的软件工程部已经在自动化定理方面做了一些工作,在 Java 代码中侦测出空指针异常。
访问 Patterns 主页,获取关于设计模式以及怎样使用它们的好的介绍。
请查看 JUnit,通过将代码 "test-infested" 来捕捉更多的错误。
关于作者
Eric Allen 毕业于 Cornell 大学,曾获得计算机科学和数学的学士学位。目前是 Cycorp, Inc. 的 Java 软件开发的带头人以及 Rice 大学编程语言小组的半工半读研究生。他的研究涉及正规语义模型和 Java 语言的扩展,都是在源代码和字节代码的级别上的。目前,他正在为 NextGen 编程语言实现一种从源代码到字节代码的编译器,这也是 Java 语言的泛型运行时类型的一种扩展。
赞助商链接