事实上有一个简单的办法来避免悬挂复合错误:给每个数据类型的基本例定义一个自己的类。我建议执行有着 LinkedList 类的链表,该类包含一个有 Empty 类或 Cons 类的域,而不是象我们前面做的那样,单个执行链接链表。这些类执行一个公共接口,如图 1 所示。
图 1. Empty 和 Cons UML 示意图
为了执行可变的方法,新的 LinkedList 类作为一个内部不可变的链表的容器,如清单 5 所示。这个步骤是必须的,因为真正的空链表没有域可变,所以它们是不可变的。
清单 5. 每个基本例获得自己的类
2 public class LinkedList {
3 private List value;
4 /**
5 * Constructs an empty LinkedList.
6 */
7 public LinkedList() { this.value = new Empty(); }
8 /**
9 * Constructs a LinkedList containing only the given element.
10 */
11 public LinkedList(Object _first) { this.value = new Cons(_first); }
12 /**
13 * Constructs a LinkedList consisting of the given Object followed by
14 * all the elements in the given LinkedList.
15 */
16 public LinkedList(Object _first, LinkedList _rest) {
17 this.value = new Cons(_first, _rest.value);
18 }
19 private LinkedList(List _value) { this.value = _value; }
20 public Object getFirst() { return this.value.getFirst(); }
21 public LinkedList getRest() { return new LinkedList(this.value.getRest()); }
22 public void addFirst(Object o) { this.value = new Cons(o, this.value); }
23 public boolean isEmpty() { return this.value instanceof Empty; }
24 public boolean equals(Object that) {
25
26 if (this.getClass() == that.getClass()) {
27 // The above test guarantees that the cast to LinkedList will always
28 // succeed.
29 return this.value.equals(((LinkedList)that).value);
30 }
31 else {
32 return false;
33 }
34 }
35 }
36
那时,执行一个不可变的链表是直截了当的,如清单 6 所示。
清单 6. 对节点作加法和乘法的方法
2 public Object getFirst();
3 public List getRest();
4 }
5 class Empty implements List {
6 public Object getFirst() { throw new NoSuchElementException(); }
7 public List getRest() { throw new NoSuchElementException(); }
8 public boolean equals(Object that) {
9 return this.getClass() == that.getClass(); }
10 }
11 class Cons implements List {
12
13 Object first;
14 List rest;
15
16 Cons(Object _first) {
17 this.first = _first;
18 this.rest = new Empty();
19 }
20 Cons(Object _first, List _rest) {
21 this.first = _first;
22 this.rest = _rest;
23 }
24 public Object getFirst() { return this.first; }
25 public List getRest() { return this.rest; }
26 public boolean equals(Object that) {
27 if (this.getClass() == that.getClass()) {
28 // The above test guarantees that the cast to Cons will always succeed.
29 Cons _that = (Cons)that;
30 boolean firstEltsMatch = this.getFirst().equals(_that.getFirst());
31 boolean restEltsMatch = this.getRest().equals(_that.getRest());
32 return firstEltsMatch && restEltsMatch;
33 }
34 else {
35 return false;
36 }
37 }
38 }
39
每一个方法的逻辑现在相当简单了。也请注意,虽然就如以前在单参数 Cons 构造器中一样,它仍旧可能引入同样的错误,但我们已经构造了一个显式 Empty 类的事实使这种可能性大大减少。另外,任何阻断我们的链表以及忽略检查空例的代码将返回一个 NoSuchElementException ,而不是那些没什么用的空指针异常。
这段代码的一个简单优化是对 Empty 类应用同一个设计类型,因为每一个 Empty 的实例都是同样的。我省去了这个优化,因为它不能相应地消除空指针异常,并且使得代码更复杂了一些。
总结
下面是这个星期的错误类型的分析:
类型:悬挂复合
症状:使用递规定义的数据类型的代码报告一个空指针异常。
原因:定义的某些基本例没有给出自己的类,然后以这种方法定义了递归数据类型。相反,空指针被插入到不同的复合数据类型。客户端代码对基本例处理不一致。
解决方法和预防措施:确保基本例的表示和检查的一致性。为每个基本例给出一个自己的类。
我们这时还不能结束对空指针问题的讨论。在下一个部分,我们将还要注视另外一个非常普遍的,也被证明是一个空指针异常的错误类型,以及怎样识别它和避免它。