目录
七、抽象函数AF(Abstract Function)和表示不变量RI(Representation Invariants)
一、什么是ADT
ADT即 Abstract Data Type ,译为抽象数据类型,经过老师的介绍加上我个人的理解,ADT其实就是我们编写的具有特定要求规范的类(class),也可以说是我们自己定义的一种数据类型,便于他人理解与使用,同时具有着一定的数据安全性。例如java中自带的String类,Set类等,他们也可以看作是ADT。
ADT具有这一些很重要的特征:是否做好信息隐藏,是否表示暴露,抽象函数AF(Abstract Function)和表示不变量RI(Representation Invariants)。程序员不仅要学会设计ADT,更要学会设计一个好的ADT。
二、数据抽象
在具体了解ADT之前,我们先来看一看“抽象”的定义,提到抽象,必然就有具体,那么对于数据来说什么是抽象,什么是具体呢?(以下仅个人理解)
我们结合着来说,对于传统的数据类型,我们关注数据本身,比如double的精确问题,int的舍位问题等等,关注它在内存中如何存储,如int是4字节,double是8字节,char是1个字节以ASCII码存储等等……我们可以看到,对于这些,我们关注的是更加底层的问题,这就是我们所说的具体。而数据抽象是由一组操作刻画的数据类型,我们对数据的具体表示并不是特别关心,而是强调“作用于数据上的操作”,我们无需知晓数据如何具体存储,只需要设计或者使用相应的操作即可,这便是抽象。
ADT更是将抽象体现的淋漓尽致,对于一个ADT来说,它内部如何存储,一些功能如何实现,这些对于使用它的用户来说都不重要,用户只需要知道这个ADT有什么样的功能就可以了,不需要知道实现这种功能所用的手段,一个ADT是由它的操作定义的。
三、ADT的类型和其中的操作类型
ADT分为mutable(可变数据类型)和immutable(不可变数据类型),这两个类型还是很好区分的,就是看class内部提没提供可以改变内部数据的值的操作,提供了便是mutable类型,没提供便是immutable类型。对于immutable类型的数据来说,我们无法改变内部的值,只能构造新的对象。
但是值得我们注意的是,变和不变是相对于用户来说的,并不是说明immutable类型的ADT内部的值无法变化(在第八节我们会详细介绍)
每个ADT其中都有相应的操作,毕竟对于ADT来说,他有什么样的操作才是更重要的事。
(1)构造器(creator):输入一个其他类型的参数,输出是当前类型的对象。就像 new ArrayList();
(2)生产器(producer):输入一个当前类型的旧对象,生成一个当前类型的新对象。例如String操作的String.concat();将两个string类型的数据连接起来,生成一个新的string类型数据,也许看到这里你会有疑问:我们都知道String是immutable类型的数据啊,但是这个操作不是改变了String吗?先别慌,我们来分析一下
String a,b;
a="aaa";
b="bbb";
a.concat(b);
String c=a.concat(b);
看上面的代码,a.concat(b)并不会修改a,而会返回一个新字符串,我们需要再用一个String类型的变量接收才行,看到这里你也许还会有疑问:那么我让a=a.concat(b);不就可以了吗,这里我们可以这样理解,a只起到以个指示的作用,一开始a指向字符串“aaa”,等到更改a后,它并不是把a指向区域中的“aaa”改为“aaabbb”,而是创造一个新的区域,让那个区域里面存储“aaabbb”,然后让a不再指向“aaa”的区域,而去指向“aaabbb”这个刚刚被创造的新区域。“aaa”并不会发生改变。这就是immutable的魅力。(这里说的太多了,不过没事我们往下看)
(3)观察器(observer):给一个当前类型对象,生成一个其他类型对象,用于观察当前类型对象。比如list.size();
(4)变值器(mutator):改变对象属性的方法,一般只在mutable类型中出现,比如list.add();变值器的返回值一般是void,因为它去改变了对象的某些内部状态。
四、设计一个好的ADT
设计一个好的ADT我们要提供一组操作,设计其行为规约。我们的操作要一致整洁,尽量能满足使用者的所有需要。比如list.size,如果没有这个操作,我们可能要自己写一个遍历来遍历整个list来获得它的长度,但是有了size,只需简单一步便可得到我们想要的结果。(虽然我们不知道size是如何实现的,但这对于我们使用它来说并不重要)
五、表示独立性
这里我一开始学的时候很懵,但是多看几遍就逐渐理解了,表示独立性就是使用者在使用我们的ADT时无需考虑其内部如何实现,ADT内部表示的变化不应该影响客户端。用一种更加令人能听明白的话说,就是我们使用ADT时,不能直接访问它内部属性,也无法直接更改。
这里就涉及到了表示暴露的概念,为了方便理解让我们来举个例子吧:
在我们设计的ADT中,用到的变量一般都要加上前置条件private,假设我们有如下代码
class F{
public List<String> a;
public List<String> Get(){
return a;
}
…… ……
}
1、假设我们现在有一个F类型的变量family,那么用户可以直接进行这样的操作:
family.a.add("Sam");
这样就可以绕过我们的ADT,直接用list的有关操作来改变ADT内部的值,这种情况在ADT中是绝对不能发生的。
2、再来看类F中的Get操作,这里就产生了一个表示暴露,Get返回了我们的list——a,那么假设我们这样来弄:
now_family=family.Get();
那么我们得到的now_family指向的就是list a,我们对now_family进行一些有关list改变的操作会直接改变a,即我们ADT内部的属性值,这个也是要坚决杜绝的,我们可以通过进行防御性复制(defensive copy,就是返回一个复制后的版本)来避免这种直接的指向,比如我们可以作如下修改:
public List<String> Get(){
return new ArrayList<>(a);
}
这样就可以避免我们的用户通过这个返回直接访问到我们的内部属性了。
六、不变性
不变性代表着在任何情况下都无法违反的规则,这个不变量由ADT本身负责,与客户端的任何行为无关。比如数据值的无法改变(immutability)就可以看成一个不变性,它有着“我们无法改变它的值”这样的一种绝对无法违反的规则。
为了保持不变性我们更应该减少表示暴露,它不仅影响独立性,也会影响不变量。最好的办法就是让我们的ADT中的各个变量都是immutable类型,这样即使直接return了,用户也不会不小心修改我们的ADT。
七、抽象函数AF(Abstract Function)和表示不变量RI(Representation Invariants)
这里是很重要的一部分内容了,描述的时候可能夹杂了许多的个人见解。
首先我们先提出两个空间,R空间(表示空间):里面是我们在ADT的实际执行的实体,可以说是在 ADT中的/输入进ADT 中的具体内容;A空间(抽象空间):即抽象值构成的空间,也就是用户看到的并使用的值。ADT开发者会关注R空间,用户只需关注A空间即可。
以上面的内容为例,R空间是字符串,提供给用户的A空间,即用户看到的是这个字符串中字符的数学集合表示形式。
这种映射关系便是抽象函数,是R和A之间映射关系的函数,即如何解释R中的每一个值为A中的每一个值。AF这种映射具有如下性质:1)必是满射;2)未必单射;3)未必双射。因为R中可能有一些不合法的值,这部分值在A中是无映射的。那么我们就可以引出表示不变性(RI)了。
RI,我们可以这样解释它:它判断某个具体的“表示”是否是“合法的”。可以把RI看作是所有表示值的一个子集,里面包含了所有合法的表示值;也可以把RI看成是一种条件,描述什么是“合法的”表示值。
举一个简单的例子,下面就是一个简单的RI和AF的定义,需要我们在代码中明确指出。
为了维持RI,我们可能在构造一个ADT的方法时,每次都要检查这个方法执行后,我们的表示值是否符合RI。
八、有益可变性
介绍完了R和A空间,我们现在能来讨论第三节中红字部分的内容了
对immutable的ADT来说,它的immutable是相对于A空间来说的,也就是说它在A空间的abstract value应是不变的,但其内部表示的R空间的取值是可以变化的。
来看这个:
这是一个ADT中的方法,这个ADT的RI要求是有理数,但当我们把结果呈现给用户时,需要得到分数的最简形式,其中numerator是分子,denominator是分母,gcd函数用于求他们的最大公因数,在这里我们发现,类中的变量numerator、denominator发生了改变(都除以最大公因数进行化简),但是这对用户得到的结果并没什么影响,无论是3/4还是6/8还是9/12最后呈现给用户的都是3/4。这种不影响A空间中值,且有利于我们实现目的的对R空间的小小变化,便是有益可变性。
但是这并不代表immutable的ADT中可以随意出现mutator
九、后记
这里就是提醒我们一定要精确给出AF和RI在我们的注释中,但是ADT的规约只能用客户可见的内容来写,包括参数。返回值和异常等,不能谈起任何内部表示细节和R空间的任何值。然后是我们的构造器和生产器在创建对象时要确保不变量为true,变值器和观察器在执行时必须保持不变性,在每个方法return之前,用checkRep()检查不变量是否得以保持。
好啦,就这些!希望能帮到对这里有一点不太懂的同学,自己也顺便复习了一下。