ADT的特性包括表示泄露、抽象函数(abstract function)和表示不变量(representation invariant),学会设计好的ADT是非常重要的。
ADT的涵义
传统的数据类型定义是关注数据的具体表示,而ADT是不同于传统数据类型的,程序员自己定义的数据类型,它是由一组操作来刻画的,不关注数据的具体表示,设计者和使用者都无需了解数据具体是怎么表示的,只需要使用相应的操作即可。
可变数据类型(mutable)和不可变数据类型(immutable)
首先要清楚这里所说的可变和不可变是针对数据所在内存处的内容来讲的,可变数据类型就是它的实例在内存中的内容不可改变,不可变同理。
例如String是不可变数据类型,因为一个字符串在内存中是不能改变的,但是我们可以通过改变实例的引用,将其指向另一个字符串来改变引用的值,实际上原来引用的位置处的内容是不变的;
基本数据类型如int也是不可变数据类型,例如对于局部变量int a = 1;,这个1是存放在栈上的,当执行a = 2;之后,只是在栈上又开辟了一处空间并放入2,然后让a指向它,并没有改变起初栈中的1;
对于List和Set,是可变数据类型,因为他们在内存中的内容是可以通过add、remove等mutator来改变的;
final对于基本数据类型和对象数据类型的修饰效果也是不一样的,对于final int a,a的值一经初始化就不能在改变,对于final List<> b; b的引用一经初始化就不能再改变,但b的内容仍然是可变的。
这一部分的理解需要分清改变值和改变引用的实际内存变化区别;
表示独立性
client使用ADT时不需要考虑内部是如何实现的,ADT的操作实现可以用多种表示方法,编写ADT可以使用任意一种方法,也可以进行改变,但是对于client来说都是无关紧要的,只需要根据ADT的规约来使用就行,同时ADT的内部表示也不应该让用户能够获取,否则当改变ADT的表示时就会出现问题。
AF和RI,表示泄露
R指表示空间,表示就是ADT内部的实现(不是操作,是变量域),表示空间是ADT设计者需要关心和实现的,A是指抽象空间,是客户看到和使用的值,在设计一个ADT时,首先我们可以得知设计的使用场景和要求,将其加工后就是抽象空间,为了设计ADT,我们需要选取合适的数据类型组合来对抽象空间进行表示,也就是构建表示空间的过程,好的表示空间可以简化方法操作的编写;
AF(abstraction function)是抽象函数,顾名思义,就是从表示空间映射到抽象空间的函数,有了AF才能将ADT的具体实现和应用场景联系起来,完成对应的操作,AF必须是满射,打不一定是单射,R空间存在一些值在抽象空间中没有对应的项。
RI(Rep invariant)表示不变量,是R空间中所有合法项的集合,是R的一个子集,可以理解为ADT的变量域需要满足的要求,限制条件。
有了以上这些,ADT的设计过程,就是先分析需求,形式化出来抽象空间,然后选取合适的表示方法,得到表示空间,接着构建二者之间的映射AF,对每一个抽象空间的项在表示空间找到一个或者多个表示,最后根据映射关系得出表示空间的合法子集,也就是RI;
表示泄露很好理解,就是表示空间的细节不能被外部获取和更改,对此,需要对所有的变量使用private和final来修饰,用private修饰之后外部无法通过实例获得变量,只能通过ADT的观察器和变值器实现,使用final的效果是让对象数据类型无法改变引用,让基本数据类型无法改变值,在观察器中最好返回内部表示的拷贝,即防御性拷贝。
通过实验2和3的练习,形成了规范写RI、AF、Safe from rep exposure的习惯,主要目的就是方便后续使用ADT。
AF、RI需要写在ADT的代码的前面,不能和方法的规约一样,方法的规约最终会体现在javadoc当中,AF、RI以及表示泄露的信息不能存在于javadoc中,也就是不能让最终使用的客户看见,这一部分内容是开发者使用ADT时可见的,方便开发者的使用。
checkRep()
做实验时一直强调要使用checkRep,checkRep就是用来实现对RI(表示不变量)的检查的,根据RI的需求在checkRep方法中对表示空间进行检查,在构造器,产生器,观察器和变值器当中所有可能改变表示空间和返回变量的地方都需要调用checkRep进行检查。