第三章 ADT与OOP——3. 抽象数据类型ADT

第三章ADT与OOP的知识清单:

1. 基本数据类型对象数据类型
2. 静态类型检查、动态类型检查
3. 值的改变、引用的改变
4. Mutable/Immutable
5. 防御式拷贝
6. snapshot diagram
7. specification、行为等价性
8. 前置后置条件
9. 规约的强度
10. ADT操作的四种类型
11. 表示独立性
12. 不变量、表示泄漏
13. 表示不变量RI、AF
14. 以注释的形式撰写AF、RI
15. 接口、抽象类、具体类
16. 继承、override
17. 多态、overload
18. 泛型
19. 等价性equals()与==
20. equals()的自反、传递、对称

21. hashCode()

22. 可变对象的观察等价性、行为等价性


10. ADT(Abstract Data Tyoe)操作的四种类型

ADT操作主要分为四种:

  • 构造器(creators):创造一个新的对象
  • 生产器(producers):从一个已有的对象中生产出一个新的对象
  • 观察器(observers):从一个抽象的对象类型中返回一个不同类型的对象
    (很简单的栗子,就是各种getXX()方法……)
  • 变值器(mutators):改变对象属性的方法。通常返回void(如果返回值为void则必然意味着他改变了对象的某些内部状态,也可能返回非空类型)

其中构造器和生产器可能会有些混淆,对于构造器而言,可能为实现的构造函数或者静态函数等,而对于生产器就是从现有的对象出发,产生一个新的对象。下面几个例子可以帮助理解一下:



11. 表示独立性(representation independence)

表示独立性:客户端(client)使用ADT时无需考虑其内部是如何实现的,ADT内部表示的变化不应影响外部的spec和客户端。

说白了就是不要客户端知道你代码的具体逻辑,他们只需要知道代码的输入应该是什么,给了合法的输入输出是什么就够了。

另外注意除非ADT操作指明了具体的前置后置条件,否则不能改变ADT的内部表示——spec规定了客户端域代码实现者(implementer)之间的契约。

当然说是不清楚的,还要给个栗子对吧:


可以看出在java doc中并没有说明代码内部是如何实现的(当然代码中也没展示出来ToT),客户只需要知道这段程序做什么的,合法的输入会得到怎样的输出就足够了。


12. 不变量(Invariants)、表示泄漏


不变量(invariants):

在任何时候都是正确的。应该由ADT来负责其不变量,与客户端的人和也行为无关。(生成一个‘person’对象总不能让客户端来设置person有两条腿吧,客户设为三条腿还好,万一设置成四五六七八条腿呢,多可怕!)

为什么需要不变量:保持程序的正确性,容易发现错误。

再多的口水也不及一个栗子呀:

首先这个是代码:


那么让我们看我们优秀的客户端是如何使用的:


天啊!这个混蛋为什么改了名字?!没错这条推特被人盗了……盗图气不气?

这也就引出了以下的概念:

表示泄漏(representation exposure):

其将代码内部的数据泄露给了客户端,这样不仅影响不变性,也影响了表示独立性(因为客户知道了你的代码内部的实现,诶呀真可怕!),当然官方的解释是:无法在不影响客户端的情况下改变其内部表示。(晦涩……)

或许你说那我把变量修饰为private,然后在加上final,这样变不了了吧。然而……呵呵哒

我们还是看个例子吧:


注意红线部分,此处Date是一个mutable的类型,这样用确实对于该成员变量的引用不会改变。然而对于它内部的值呢?


看此时的快照图:


就算你是final修饰又如何?骨子里善变该变还是要变的。所以对于这种mutable变量,防御式拷贝也就起了重要的作用:


这样就万事大吉啦,另外构造方法处也要记得修改。当然在上一节已经详细说明了,就不再赘余了……


13. 表示不变量RI、抽象函数AF

首先先来介绍一下表示空间R与抽象空间A:

  • 表示空间(the space of representation values):表示值(rep 值)的空间,由实际实现实体的值组成。一般情况下ADT的表示比较简单,有些时候也会用比较复杂的表示。
  • 抽象空间(the space of abstract values):抽象值构成的空间:用户看到和使用的值。

ADT实现者关注表示空间R,用户关注抽象空间A。

注意:R->A必须是满射,但可以不是单射。

抽象函数:R与A之间的映射关系的函数。

表示不变量RI:某个具体的“表示”是否是“合法的”,也可以把RI看做所有表示值的一个子集,包含了所有合法的表示值或者看成描述了什么是“合法”的表示值的一个条件。


另外,不同的内部表示需要设计不同的AF和RI,选择某种特定的表示方式R,进而指定某个子集是“合法”得RI,并为盖子集中的每个值做出一个“解释”AF——即如何映射到抽象空间中的值。

即使是同样的R、同样的RI,也可能有不同的AF,即“解释不同”。


设计ADT步骤:

  1. 选择表示空间R与抽象空间A
  2. 给出RI——合法的表示值
  3. 如何解释合法的表示值——映射AF


同样给出再多定义不如一道例题收获的多:



来解释一下这道例题:

第一个RI:s序列中的元素有小到大排序,而且不允许相同

第二个RI:s的长度为奇数,s序列中的元素有小到大排序,但允许相同

第三个RI:s序列中的元素不允许相同

第四个RI:null

现来看add方法:其在s后面添加了一个c字符,那么根据c的不同那么分别就不支持了第一二三个RI。

然后来看remove方法:注意,这个方法中只删除了第一个c,对于之后c不删除,所以如果s中存在重复字符,那么就不满足remove的spec。所以其只支持一三。

再来一个栗子:


乍一看好像四个都可以,但其实注意该方法的实现,这里遍历操作是一次扫描两个字符,所以当s为奇数个字符时,会出现空指针异常,所以只能选第二个。

不同的内部表示,需要设计不同的AF与RI。

选择某种特定的表示方式R,进而指定某个子集是合法的RI,并为该子集中的每个值做出解释”AF“即如何映射到抽象空间中的值。

同样的R、同样的RI,也可能有这不同的AF,即解释不同。

猜你喜欢

转载自blog.csdn.net/qq_37549266/article/details/80707151
今日推荐