抽象数据类型(ADT)
Abstraction and User-Defined Types
编程语言提供的基本数据类型和对象数据类型有限,程序员可以根据自己的需求,定义属于自己的数据类型。
数据抽象是由一组操作所刻画的数据类型,例如一个number类型,是满足可以进行加操作和乘操作的。
传统类型定义更关注数据的具体表示,并非操作,而抽象类型强调数据上的操作,无需关心数据的存储,是需要完成设计、使用操作即可。
Classifying Types and Operations
可变类型的对象,提供了可改变其内部数据的值的操作,例如Java中的Date、StringBuilder类就是可变类型;不可变类型的对象,不提供可改变内部值的操作,而是构造新的对象,例如Java中的String类就是不可变类型。
ADT中操作被分为如下几种:
1.构造器Creators,创造该类型新的对象,可能实现为构造函数或静态函数;
2.生产器Producer,从该类型旧对象中创建新对象,如String的concat()方法;
3.观察器Observers,获取抽象类型对象并返回不同类型的对象,如List类的size()方法;
4.变值器Mutators,改变对象属性的方法,如List类的add()方法。变值器通常返回void;
Abstract Data Type Examples
int类型是不可变的,其操作如下:
1.Creators,数字文字,如0,1,2,…
2.Producers,算术操作符,如+,-,*,/
3.Observers,比较运算符,如==,!=,<,>
4.Mutators,无
String是Java的字符串类型,是不可变的,其操作如下:
1.Creators,字符串构造器
2.Producers,concat,substring,toUpperCase
3.Observers,length,charAt
4.Mutators,无
List是Java的列表类型,是可变的,同时也是一个接口,为其他类提供了实现的数据类型,如ArrayList、LinkedList,其操作如下:
1.Creators,ArrayList、LinkedList构造器,Collections.singletonList
2.Producers,Collections.unmodifiableList
3.Observers,size,get
4.Mutators,add,remove,addAll,Collections.sort
具体例子如下:
操作 | 操作类型 |
---|---|
Integer.valueOf() | Creator |
BigInteger.mod() | Producer |
List.addAll() | Mutator |
String.toUpperCase() | Producer |
Set.contains() | Observer |
Map.keySet() | Observer |
Collections.unmodifiableList() | Producer |
BufferedReader.readLine() | Mutator |
Designing an Abstract Type
良好ADT的设计靠“经验法则”,提供一组操作,设计其行为规约spec。
1.设计简洁、一致的操作。最好使用简单的操作实现,而非复杂的操作;每个操作都应该有明确的目的,且有连贯的行为,而不是一大堆特殊情况,操作行为应该是内聚的。
2.足以支持client对数据所做的所有操作需要,操作满足client需要的难度要低。对象的每个需要被访问的属性应该都能被访问到;基本信息应该易于获取。
3.要么抽象,要么具体,不能混合。面向具体应用的类型不应该包含通用方法,而面向通用的类型不应包含面向具体应用的方法。
Representation Independence
表示独立性是client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应影响外部spec和客户端。
通过前提条件和后置条件充分刻画ADT的操作,spec规定client和实现者的契约,明确了client知道可以依赖哪些内容,实现者知道可以安全更改的内容。
Testing an Abstract Data Type
测试ADT方法:
测试Creators、Producers和Mutators通过调用Observers来观察这些操作结果是否满足规约;测试Observers通过调用Creators、Producers和Mutators等方法产生或改变对象,来看结果是否正确。
例如:
但是这样存在风险,如果被依赖的其他方法有错误,可能导致被测试方法的测试结果失效。
Invariants
不变量,程序在任何时候总是true的性质,一个好的ADT的属性是需要始终保持其不变量。不变量是由ADT负责,与client端的行为无关。
不变量可以保持程序的“正确性”,易于发现错误。在设计时要假设client可能存在“恶意”毁坏ADT不变量行为。
可变类型一种危险是客户端可以直接访问它的域,例如:
/**
* This immutable data type represents a twwet from Twitter.
*/
public class Tweet{
public String author;
public String text;
public Date timestamp;
/**
* Make a Tweet.
* @param author Twitter user who wrote the tweet
* @param text text of the tweet
* @param timestampe data/time when the tweet was sent
*/
public Tweet(String author,String text,Date timestamp){
this.author = author;
this.text = text;
this.timestamp = timestamp;
}
}
Tweet t = new Tweet("abc","Hello,world!",new Date());
t.author = "ABU";
这种情况是表示泄露,不仅影响不变量,也影响了表示独立性,无法在不影响客户端的情况下改变其内部表示。
关键字private可以表示fields和methods仅可在类内部访问,而关键字final则说明fields在第一次赋值后不能修改,否则会编译报错。用private和final修饰则可以维持它的不变性。
当可变对象复制代价过高时候,在签名中声明客户端不能改变。但是,不要把希望过多给客户端,ADT有责任保证不变性、避免表示泄露,最好就是使用不可变类型,例如用java.time.ZonedDateTime代替java.util.Date。
总结一下,不要将可变参数合并到对象中,要使用防御性的副本;可变字段返回时返回防御副本;返回新的实例而不是去修改原来的实例,或者返回可变字段的不可修改视图;使用不可变成分来消除防御性副本的需要。
Rep Invariant abd Abstraciton Function
R为表示值构成的空间,即表示空间,实现者看到和使用的值;A为抽象值构成的空间,即抽象空间,client看到和使用的值。
AF是抽象函数,是R和A之间映射关系的函数,即如何将R中的每一个值解释为A中的每一个值。
AF是满射,不是单射(R中部分值可能不合法,缺少A中对应的映射值),因此不一定是双射。
RI是values值到booleans的映射,对于每一个值r,RI®是true如果r可以被AF映射,换言之RI说明哪些值是合法的,它是所有表示值的一个子集,包含了所有合法的表示值。
例如:
public class CharSet{
private String s;
// Rep invariant:
// s contains no repeated characters
// Abstraction function:
// AF(s) = {s[i] | 0<= i <s.length()}
...
}
同一个ADT可以有多种表示,对于不同的表示需要设计不同的AF和RI。确定某种特定的表示方式R,进一步指定某个子集是合法的(设计RI),并未该子集内的每个值做出解释(设计AF)。
同样的表示空间R,可以有不同的RI;同样的R、同样的RI,也可以有不同的AF。
设计ADT,首先要选择空间R和空间A,再设计RI完成合法的表示值,最后再对合法的表示值进行解释,完成AF的设计。
在每一个操作的最后都要进行不变性检查,可以通过实现private void checkRep(),重复调用此方法进行检查,避免表示泄露。
Beneficent mutation
对于不可变的ADT,它在A空间的抽象值是应该保持不变的,但是内部表示的R空间的值是可以变化的。这种是有益的可变性。
例如:
@Override
public String toString(){
int g = gcd(numerator,denomitnator);
numerator /= g;
denominator /= g;
if(denominator < 0){
numerator = -numerator;
denominator = -denominator;
}
checkRep();
return (denominator > 1) ? (numerator + "/" + denominator) : (numerator + "");
弱化RI,参与运算时,可以有公约数,而显示输出的时候,需要化简至没有公约数。
重写的toString重新分配了私有字段分子和分母,改变表示,即使它是不可变类型上的Observers,但是它不改变抽象值。这种改变只改变了R值未改变A值,在client视角是没有改变的,这种mutation是无害甚至是有益的。
但是这种不变类型中仍然不可以随意出现mutator。
Documenting the AF,RI,and Safety from Rep Exposure
在代码中用注释形式记录AF和RI,分别解释每一个R值、rep中的fields的值的有效性。另外,也要给出表示泄露的安全声明Rep exposure safety argument,给出证明代码未对外泄露内部表示的理由。
这里区分一下规约,规约里只能使用client课间的内容撰写,如参数、返回值、异常等,值只能为A空间的值。
规约中不应该有任何内部的细节以及空间的任何值,私有属性对外部应该严格不可见。
在代码中注释形式写的AF和RI不会在Javadoc文档中,防止被外部看到破坏表示独立性。
为了保持不变性,对象初始状态不变量为true,在对象发生变化时,不变量也要为true;Creators和Producers在创建对象时也要保持不变量为true;Mutators和Observers在执行时也要保持不变性;在每个方法返回前,使用实现的checkRep()检查不变量是否保持。
另外,一旦表示泄露,ADT内部表示可能在程序任何位置改变(不一定是ADT内部),无法确保ADT的不变量是否始终为true。
ADT invariants replace preconditions
ADT不变量可以取代复杂的前置条件(相当于把其封装到ADT内部)。
所需条件可在一个位置强制执行,而Java静态检查发挥作用,如果值不符合条件则编译时报错。
这更容易理解,传达了程序要需要知道的内容;这更容易改变,无需更改client。