可以使用UML将方法表示为:
且将这行添加到类图中。
可以使用一个布尔值方法来测试包是否为空,同样该方法没有参数。用伪代码及UML描述这个方法的规格说明
及
将这行添加到类图中。
注:因为通过查看getCurrentSize是否返回0就能检测包何时为空,所以并不真的需要操作isEmpty。但是它是所谓的便利方法(convenience method),所以很多集合都提供这样一个操作。
现在想向包中添加给定的对象。可以将这个方法命名为add,并有一个表示新项的参数。可以写出下列伪代码:
我们可能试图让add作为void方法,但是会有一些情况,比如如果包满则不能将新项添加到包中。这些情况下,我们该如何办呢?
设计决策:当不能添加新项时,方法add将如何处理?
当add不能完成任务时,我们可采取下面两种选择:
什么也不做。不能添加其他的项,所以忽略这个项并且不改变包。
不改变包,但告诉客户添加是不可能的。
第一个选择简单,但会让客户疑惑到底发生了什么。当然,我们可以规定add的前置条件,即包必须不满。这样客户要负责避免将新项添加到满包中。
第二个选择更好一些,且它也不难说明或实现。我们如何告诉客户添加是否成功?标准Java接口Collection规定,如果添加没有成功则发生异常。稍后我们再完成这个方法,并且使用另一种方式。显示一条错误信息并不是好的选择,因为你应该让客户决定所有的书面输出。因为添加操作或者成功或者不成功,所以我们可以让方法add返回一个布尔值。
因此,可以用UML规范add方法:
其中T表
示newEntry的数据类型。
自测题1 假定aBag表示一个有有限容量的空包。写伪代码,将用户提供的字符串添加到包中,直到操作失败。
有3个动作涉及从包中删除项:删除所有的项;删除任意一项;删除某个项。假定我们用伪代码为这些方法命名并说明其参数,如下所示:
这些方法的返回类型是什么?
方法clear可以是一个void方法:我们只想要一个空包,不获取它的任何内容。所以,在UML中方法写为:
如果第一个remove方法从包中删除一项,则该方法可以简单地返回被删除的对象。它的返回类型为T,这是包中项的数据类型。在UML中,我们有
现在,我们可以处理从返回null的空包中删除对象了。
如果包中不含有某项,则第二个remove方法不能从包中删除该项。可以让方法返回一个布尔值,类似于add那样,用它来表示成功与否。或者,方法可以返回被删对象,或者,如果不能删除这个对象则返回null。下面是用UML表示的规格说明的两种可能版本——我们必须二选一:
或者
如果anEntry等于包中的某项,则这个方法的第一个版本将删除该项并返回真(true)。即使方法没有返回被删除的项,客户也能有方法的参数anEntry,它等于被删除的项。故我们选择这个版本,它与接口Collection是一致的。
自测题2 在一个类内同时具有上面描述的remove(anEntry)的两个版本合法吗?
解释。
自测题3 在一个类内同时具有remove的两个版本,一个不带参数而另一个带一个参数,这样合法吗?解释。
自测题4 给出自测题1中创建的满包aBag,写伪代码语句,删除并显示包中的所有字符串。
其他的动作并不改变包的内容。其中一个动作是计数包中给定对象的出现次数。我们先用伪代码后用UML说明它,如下所示。
另一个方法测试包是否含有给定对象。使用伪代码和UML给出的规格说明如下所示。
自测题5 给定自测题1中创建的满包aBag,写伪代码语句,找出aBag中字符串"Hello"出现的次数,如果有的话。
最后,我们想看看包的内容。不是提供显示包中项的方法,而是定义一个方法来返回保存这些项的数组。这样,客户可以按照自己的意愿显示部分或全部的项。下面是最后这个方法的规格说明:
当方法返回一个数组时,它通常应该定义一个新的数组来返回。我们还将说明这个方法的细节。
当我们为包中的方法提供前面那些规格说明时,使用UML符号来表示它们。图1-2显示了这些结果。
注意,CRC卡和UML并不反映所有的细节,例如我们在前面的讨论中提到过的假定和特殊情形。但是,在确定了这样的条件后,你应该在每个方法的下面说明该方法应有的动作。
应该写下你的决策,想让方法如何动作,就像我们写在下表中的那样。然后,可以将这些非形式化的描述放在说明方法的Java注释中。
设计决策:当特殊条件出现时会怎样?
作为类的设计者,必须要做出决定如何处理特殊条件,并将这些决策包含在规格说明中。ADT包的文档应该反映这些决策和前面讨论的细节。
一般地,可以用几种方式声明特殊情形。你的方法可能
- 假定无效的情形不能发生。这个假定并不像听起来那么幼稚。方法可以声明一种假设(即前置条件),这是客户必须遵守的限制。然后由客户检查在方法调用前这个前置条件是否满足。例如,方法remove的前置条件可能是包为非空的。注意,客户可以使用ADT包的其他方法,例如isEmpty和getCurrentSize,来辅助完成这个任务。只要客户遵守这个限制,无效的情形就不会发生。
- 忽略无效情形。当给出无效数据时方法可能简单到什么也不做。但是什么都不做会让客户不知道发生了什么。
- 猜测客户的意图。与前一个选择一样,这个选择可能为客户带来麻烦。
返回一个表示问题的值。例如,如果客户试图从空包中remove一项时,remove方法应该返回null。返回的值必须是不在包中的值。 - 返回一个布尔值,表示操作的成功或失败。
- 抛出一个异常。
注:抛出异常经常是Java方法运行期间处理遇到的特殊事件的理想方法。方法可以简单地报告问题而不决定要做什么。异常能让每个客户根据自己的特殊情形按需处理。Java插曲2将介绍异常的基本机制。
注:ADT规格说明的草稿经常忽视或忽略你确实需要考虑的情形。你可能为了简化草稿而有意忽略这些。一旦写好了规格说明中的大部分内容,就可以关注这些细节,而让规格说明更完善。
一个接口
随着规格说明越来越详细,也越发地影响到你对程序设计语言的选择。最终,你可能为包的方法写下Java的方法头并将它们组织为一个Java接口,用它们来实现ADT的类。程序清单1-1中的Java接口含有ADT包的方法及描述它们行为的详细注释。回想一下,类接口不含有数据域、构造方法、私有方法或保护方法。
现在,包中的项将是同一个类的对象。例如,我们可以有字符串的包。为了容纳类类型的项,包的方法中使用泛型数据类型(generic data type)T>来表示每个项。必须在接口名的后面写,来说明标识符T的含义。一旦客户选择了具体的数据类型,编译程序将在T出现的所有地方使用那个数据类型。接在本章后面的Java插曲1中,将讨论如何使用泛型为ADT中的数据提供类型的灵活性。
当检查接口时,注意前一段中提到的处理特殊情形时所做的决策。具体来说,对于add、remove及contains方法,它们每一个都返回一个值。因为我们的程序设计语言是Java,所以要注意,有一个remove方法返回一个指向项的引用,而不是项本身。
虽然不一定要在实现类之前写接口,但这样做能让你以简洁的方式记录你的规格说明。然后可以将接口中的代码用在具体类的框架中。有了接口还能为包提供数据类型,它不依赖于具体的类定义。接下来的两章将开发包类的两种不同的实现。针对接口所写的代码,能让我们更易于将包的一种实现替换为另一种。
程序清单1-1 包类的Java接口
说明一个ADT并为它的操作写了Java接口后,应该写几个使用ADT的Java语句。虽然还不能执行这些语句(毕竟我们没写实现BagInterface的类),但我们可以用它们来确认或者修改方法的设计决策及相关文档。这样,可以检查规格说明的适应性及对它的理解。最好现在来修改ADT的设计或文档,而不是等到写完实现后再进行。认真做这件事的额外好处是,后面可以使用这些相同的Java语句来测试你的实现。
自测题6 给定自测题1创建的包aBag,写Java语句,显示aBag中所有的字符串。不要改变aBag的内容。
程序设计技巧:在实现一个类之前写测试程序
写Java语句来测试一个类的方法,将有助于你完全理解方法的规格说明。很明显,在能正确实现方法之前必须理解它。如果你也是类的设计者,那么使用这个类可能有助于你对设计或对文档进行理想的修改。如果在实现类之前做这些修改,将会节省时间。因为早晚都要写一个程序来测试你的实现,所以为什么不现在写而获益,而非要放到以后再写呢?
注:虽然我们说过,包中的项属于同一个类,但这些项也可能属于因继承关系而相关的类。例如,假定Bag是实现接口BagInterface的类。如果我们写下面的语句创建类C对象的包:
则aBag中可以包含类C的对象及C的任何子类的对象。
下一节看看使用包的两个例子。后面,可以用这些例子来测试你的实现。