在设计或实现某种集合的时候,经常会遇到集合边界值的情况。例如,从集合中取一个元素,必须得考虑这个集合是否非空;向一个有限集合添加元素,必须考虑集合满的情况。
而对于包含异常处理的编程语言来说,如果遇到集合操作边界,我们常见的编程约定有两种:
一种选择是将边界条件作为异常来处理。如下面处理集合为空的代码。
代码1:
Collection c;
//doSomething();
Item i;
try {
i = c.get(); //take a item from the collection
}
catch (GetItemException e) {
log.err("Cannot get item from a null collection.");
raise e;
}
//use the item;
另一种选择是不将边界操作作为异常来处理,而是用某个特定值来表示(如null)。
代码2:
Item i = c.get();
if (i == null) {
i = DefaultValue;
}
//use the Item i
对于究竟是否要将这类边界操作作为错误来处理,依程序的业务逻辑而定。有时,我们希望将其作为异常来处理;有时我们希望只是作为普通的逻辑判断条件,用一个特定的值代表最好。
如果在设计集合的接口时,把所有边界操作作为异常处理,那么要实现普通的逻辑判断就要麻烦一点了。
代码3:
try {
i = c.get();
}
catch (GetItemException e) {
i = DefaultValue;
}
当然,如果集合提供了IsEmpty()操作,上述的代码可以更优雅一些:
代码4:
i = c.isEmpty() ? DefaultValue : c.get();
当代码4与代码3相比,含义稍有不同。代码3适应于所有无法获取集合成员的情况,而不仅仅是集合为空的情况(例如,多线程并发获取集合成员时,某个线程等待锁超时);代码4只能处理集合为空的情况。所以,代码4把集合接口的抽象程度降低了。
当然,接口的抽象程度并非越高越好,而是根据集合的应用环境而定的。这里只是指出了不同设计的差异。
JDK的Collection接口设计时,采用第一种约定。对于add/remove方法,采用抛出异常的方式处理边界操作。
JDK的Queue接口设计时,同时采用两种约定,提供两类接口。一类传统的继承了Collection抛出异常的方式,即add/remove方法;另一类使用返回特定值(false/null)的方式处理边界条件,即offer/poll方法。
然而,事情还没有结束。并发编程时,常常用到类似于阻塞队列的数据结构。当队列为空时,获取元素的的操作结果不是null,也不是抛出一个异常,而是调用线程应该被阻塞。因此,JDK 5以后,在java.util.concurrent包里,又引入了put/take方法,来专门处理这里的第三种情况。
小结,对于集合的增加/删除操作,考虑到集合边界条件(空/满),调用约定通常有三种。以JDK为例:
add/remove: 达到边界条件,抛出异常;
offer/poll:达到边界条件,返回特定常量(false/null);
put/take: 达到边界条件,阻塞调用线程。