抽象数据类型 ADT
0.抽象数据类型和表示独立性:
- 理解抽象数据类型:一个数学模型和在其上定义的操作
(类比OOP:对象和在对象上定义的操作) - 理解表示独立性:将使用和其内部表示分离开来
- 原因:用户使用ADT时猜测其内部如何表示时很危险的事情,一旦用户猜测错误或内部实现修改,用户的意图将无法实现,从而产生Bug
1.抽象和用户自定义类型
比较传统类型定义和数据抽象
- 传统类型:用户需要关注数据的具体表示
- 抽象类型:用户无需关心数据如何表示,只需使用操作
举例:
true : Bool
false : Bool
and : Bool × \times × Bool → \rightarrow → Bool
or : Bool × \times × Bool → \rightarrow → Bool
not :Bool → \rightarrow → Bool
无需关注Bool类型实际如何表示
抽象数据类型是用其操作定义的,与其内部实现无关
T的操作和规约刻画了其特征
举例:
当我们讨论List时,并不是指LinkedList 或array,实际上指的是一种完全不透明的类型,我们只需要知道其有get(),size()等方法,并不关注其内容具体是什么样的
2.类型与操作的分类
(1)类型的分类
无论是内置类型还是用户定义的类型,都可以分为可变类型和不可变类型,
- 可变类型:提供了课改变其内部数据值的操作
举例:Date是可变类型,引起提供了setMonth()和getMonth()的操作 - 不可变类型: 操作不可改变其内部值,而是构造新的对象
有的时候一种类型会提供两种形式,可变的或不可变的,比如Java中,String是可变的,Stringbuilder是不可变的,但是Java中它们并不是同一种类型
(2)操作的分类
- Creator 构造器(从无到有)
- Producers 生产器(从有到新)
- Observers 观察器
- Mutators 变值器
符号定义
T 抽象数据类型本身
t 其他类型
* 0个或多个
+ 1个或多个
| 或
Creators构造器
t ∗ → T t* \rightarrow T t∗→T
Creator可能有参数,但参数中不能有待创建的类型
可以实现为
- 构造函数,如new ArrayList()
- 静态方法,如Arrays.asList()
实现为静态方法的构造器通常称为工厂方法
如String.valueOf()(与Object.toString()相对)
Producers生产器
T
+
,
t
∗
→
T
T+,t*\rightarrow T
T+,t∗→T
举例:String.concat()
Observers观察器
T
+
,
t
∗
→
t
T+,t* \rightarrow t
T+,t∗→t
举例:size(),rigionMatches()
Mutators变值器
T
+
,
t
∗
→
v
o
i
d
∣
t
∣
T
T+,t*\rightarrow void|t|T
T+,t∗→void∣t∣T
变值器通常返回void,如果返回void,则必然意味着他改变了对象的某些内部状态
变值器也可能返回非空类型
举例:Set.add()返回boolean类型,用于表示set是否改变,Component.add()返回对象本身,用于链式调用
3.ADT举例
int,String,List
int
int 是不可变类型是,所以没有变值器
- creators 构造器: 数字字符0,1,2,…
- producers生产器:算术运算,+,-,*,/
- observers观察器: 比较运算 ==,!=,<,>
- mutators变值器:不存在(int是不可变类型)
String
String 是不可变类型是,所以没有变值器
- creators 构造器: String构造器
- producers生产器:concat,substirng,toUpperCase
- observers观察器: length,charAt
- mutators变值器:不存在(String是不可变类型)
List
- creators 构造器: ArrayList,LinkedList构造器,Collections.singletonList
- producers生产器:Collections.unmodifiableList()
- observers观察器: size(),get()
- mutators变值器:add(),remove(),addAll(),Collections.sort()
举例:
Method | Classification |
---|---|
Ineger.valueOf() | Creator |
DigInteger.mod() | Producer |
List.addAll() | Mutator |
String.toUpperCase() | Producer |
Set.contains() | Observer |
Map.keySet() | Observer |
Collections.unmodifiableLIst() | Producer |
BufferedReader.readLine() | Mutator |
4.设计ADT
设计ADT包括选择一组好的操作,并决定它们如何组合起来
经验法则1 操作简洁一致
操作应当简单,并通过简单操作的组合实现复杂的操作,操作的行为是内聚的,一致的,即不为特殊的使用情况提供操作。
举例:List 中不应当有sum操作,因为string等类型无法执行sum操作
经验法则2 操作要满足用户所有需要,且用户使用难度要低
一种好的判断能否满足用户需求的方法是看每个需要的属性是否都能被用户访问到
如List.get()
基本信息应该容易被访问,如size,让用户去遍历太复杂,应该提供size()操作
经验法则3 要么抽象,要么具体,不要混合
面向通用的类型,如list,set,不应该包含通用的方法
面向具体的应用类型,如employee datebase,streetmap ,不应该包含通用的方法
5.表示独立性
表示独立性(Representation Independence)
客户端(client)使用ADT时无需考虑其内部如何实现,ADT内部变化不应影响外部规约和客户端
举例:ADT List 提供的操作表现的内容与其用linkedlist还是array无关
只有当前置条件和后置条件充分的时候,才可以修改ADT的内部实现,这样,客户端(client)知道可以依赖什么,而ADT的设计维护者也可以知道如何安全的修改ADT
specification规约(类,字段,方法前)
representation表现(用于字段)
implementation实现(用于方法)
举例:MyString类
MyStirng类定义子串方法substring()返回子串
一种定义方法是
public class MyString{
private char[] a;
...
public MyString substring(int start,int end){
//创建一个新对象
MyString that = new MyString();
//创建一个新的char数组
that.a = new char[end - start];
//将原来数组中对应内容拷贝到新数组中
System.arraycopy(this.a,start,that.a,0,end-start);
//将新数组返回
return that;
}
}
由于Mystring被定义为不可变的(immutable),所以子串无需一个个字符进行拷贝,因此可以有另一种实现
public class MyString{
private char[] a;
private int start;
private int end;
...
pubilc MyString substring(int start,int end){
MyString that = new MyString();
//仍然使用原来的字符串
that.a = this.a;
//修改始末位置,以定义新的字符串
that.start = this.start + start;
that.end = this.end + end;
return that;
}
}
由于客户端(client)只关注public方法,而不关注private字段,这样的改变不用修改客户端(client)写好的代码
这就是表示独立性(Representation Independence)的好处
6.测试抽象数据类型(ADT)
如何测试抽象数据类型?
测试抽象数据类型的某个操作,不可避免的与测试该抽象数据类型的其他操作有联系
- 测试构造器(creators),生产器(producers),变值器(mutators)的唯一方法是调用观察器(observers)看,看结果是否和规约一致
- 测试观察器(observers)的唯一方法是调用构造器(creators),生产器(producers),变值器(mutators)来改变对象,看结果是否正确
风险:如果被依赖的其他方法有错误,可能导致被测试方法的测试结果失效
7.不变量(Invariants)
不变量,即在任何时候总是true的性质,比如immutability就是典型的不变量。
好的ADT总是要保持其不变量,ADT的不变量是由ADT自身负责的,与客户端(client)行为无关。
使用不变量可以使分析代码变得容易。
举例:String有immutable这个不变量,如果没有这个不变量,那么每次使用String就都要检查String对象是否发生变化
总是要假设用户尝试着破坏不变量(defensive programming)
影响不变量的因素
表示泄露(representation exposure):
定义:类外的代码可以直接修改/使用字段
不仅影响不变量,而且影响表示独立性,无法在不影响客户端的情况下改变其内部表示
举例:
- 情景1
public class Tweet{
public String author;
public String text;
public Date timestamp;
...
}
如果改变Tweet的实现,极大可能影响客户端的代码
解决方法:使用private 和 final
private用于确定访问权限
final 用于将对象指明为不可变(immutable)的类型
private final String author;
private final String text;
private final Date timestamp;
- 情景2
public static Tweet retweetLater(Tweet t){
Date d = t.getTimestamp();
d.setHours(d.getHours()+1);
return new Tweet("rbmllr",t.getText(),d);
}
导致问题的原因是,Tweet泄露了一个可变类型(Date)的引用,虽然我们已经声明了timestamp是final的,但是其引用的内容可以修改,因此不可变(immutable)的性质未能被保护
解决方法
- 使用不可变类型
- 对可变类型进行防御式拷贝(defensive copying)
copy constructor,或者clone()
return new Date(timestamp.getTime());
分析以下代码
public static List<Tweet> tweetEveryHourToday(){
List<Tweet> list = new ArrayList<Tweet>();
Date date = new Date();
for(int i = 0; i < 24,i++){
date.setHours(i);
list.add(new Tweet("rbmllr","keep it up! you can do it"),date);
}
return list;
}
对Tweet的构造方法实现要留意,如果不进行防御式拷贝,所有Tweet时间戳都将是同一时间
public Tweet(String author,String text,Date timestamp){
this.author = author;
this.text = text;
this.timestamp = new Date(timestamp.getTime());
}
当复制代价很高时,不得不在规约中限制客户端不能修改相关内容,但是由此引发的程序分析,防止bug的代价也很高
/**
*Make a Tweet.
*@param author Twitter users who wrote the tweet
*@param text text of the tweet
*@param timestamp date/time when the tweet was sent.Caller must never mutate this Date object again!
*
*/
public Tweet(String author,String text,Date timestamp){
...
}
8.表示不变性(Rep Invariant,RI)和抽象函数(Abstraction Function,AF)
两个空间
表示空间R:rep values,实现着看到和使用的值
抽象空间A:abstract function,理想的,用户视图角度的
R与A的关系
R与A间一定是满射,未必是单射,未必是双射
表示不变性RI
R
I
:
R
→
b
o
o
l
e
a
n
RI:R\rightarrow boolean
RI:R→boolean
对一个表示值r,RI®是true当且仅当RI®被映射到AF中
RI可以看做合法值的条件,也可以看成合法值的集合
与RI和AF相关的表示
RI和AF与内部表示有关,不同的内部表示,RI和AF将不同。
选择某种特定表示方式R,进而指定某个子集是“合法”的,并为该子集中每个值作出解释(AF)
同样的表示空间R,可能有不同的RI
即使是同样的R,同样的RI,也可能有不同的AF
设计ADT:
(1)选择R和A
(2)RI 合法的表示值
(3)AF 如何解释合法的表示值
要将这些选择和解释明确的写到代码中
表示不变量在所有可能改变rep的方法内部都要检查(checkrep()),使用assert,Observer可以不检查,但也建议检查,以防万一
9.有益的可变量(Beneficient mutation)
对immutable的ADT来说,其在A空间中的取值应当是永远不变的,但是其在R空间的取值可以变化。这种变化不能改变AF,(由于R到A非单射,对AF中相同的值,可以在对应多个R中的值中变化)。这样可以换取效率和性能。
10.记录AF,RI,和防止表示泄露(Documenting the AF,RI,and Safety from Rep Exposure)
精确记录RI:rep中所有field何为有效
精确记录AF:如何解释每一个R值
表示泄露声明:给出理由,证明代码并未对外泄露其内部表示
11.用ADT不变量取代前提条件(ADT invariants replace preconditions)
用ADT不变量取代前提条件,相当于将复杂的前提条件封装到ADT内部,更加安全,更容易理解,更能适应变化