第三章第三节 抽象数据型(ADT)
3-1节研究了“数据类型”及其特性 ; 3-2节研究了方法和操作的“规约”及其特性;在本节中,我们将数据和操作复合起来,构成ADT,学习ADT的核心特征,以及如何设计“好的”ADT。
Outline
- ADT及其四种类型
- ADT的基本概念
- ADT的四种类型
- 设计一个好的ADT
- 表示独立性
- ADT的特性
- 不变量
- 表示泄露
- 抽象函数AF
- 表示不变量RI
- 以注释的形式撰写AF、RI
Notes
## ADT及其四种类型
【ADT的基本概念】
- 抽象数据类型(Abstract Data Type,ADT)是是指一个数学模型以及定义在该模型上的一组操作;即包括数据数据元素,数据关系以及相关的操作。
- ADT具有以下几个能表达抽象思想的词:
- 抽象化:用更简单、更高级的思想省略或隐藏低级细节。
- 模块化: 将系统划分为组件或模块,每个组件可以设计,实施,测试,推理和重用,与系统其余部分分开使用。
- 封装:围绕模块构建墙,以便模块负责自身的内部行为,并且系统其他部分的错误不会损坏其完整性。
- 信息隐藏: 从系统其余部分隐藏模块实现的细节,以便稍后可以更改这些细节,而无需更改系统的其他部分。
- 关注点分离: 一个功能只是单个模块的责任,而不跨越多个模块。
- 与传统类型定义的差别:
- 传统的类型定义:关注数据的具体表示。
- 抽象类型:强调“作用于数据上的操作”,程序员和client无需关心数据如何具体存储的,只需设计/使用操作即可。
-
ADT是由操作定义的,与其内部如何实现无关!
【ADT的四种类型】
- 前置定义:mutable and immutable types
- 可变类型的对象:提供了可改变其内部数据的值的操作。Date
- 不变数据类型: 其操作不改变内部值,而是构造新的对象。String
- Creators(构造器):
- 创建某个类型的新对象,⼀个创建者可能会接受⼀个对象作为参数,但是这个对象的类型不能是它创建对象对应的类型。可能实现为构造函数或静态函数。(通常称为工厂方法)
- t* -> T
- 栗子:Integer.valueOf( )
- Producers(生产器):
- 通过接受同类型的对象创建新的对象。
- T+ , t* -> T
- 栗子:String.concat( )
- Observers(观察器):
- 获取抽象类型的对象然后返回一个不同类型的对象/值。
- T+ , t* -> t
- 栗子:List.size( ) ;
- Mutators(变值器):
- 改变对象属性的方法 ,
- 变值器通常返回void,若为void,则必然意味着它改变了对象的某些内部状态;当然,也可能返回非空类型
- T+ , t* -> t || T || void
- 栗子:List.add( )
- 解释:T是ADT本身;t是其他类型;+ 表示这个类型可能出现一次或多次;* 表示可能出现0次或多次。
- 更多栗子:
【设计一个好的ADT】
设计好的ADT,靠“经验法则”,提供一组操作,设计其行为规约 spec
- 原则 1:设计简洁、一致的操作。
- 最好有一些简单的操作,它们可以以强大的方式组合,而不是很多复杂的操作。
- 每个操作应该有明确的目的,并且应该有一致的行为而不是一连串的特殊情况。
- 原则 2:要足以支持用户对数据所做的所有操作需要,且用操作满足用户需要的难度要低。
- 提供get()操作以获得list内部数据
- 提供size()操作获取list的长度
- 原则 3:要么抽象、要么具体,不要混合 —— 要么针对抽象设计,要么针对具体应用的设计。
【测试ADT】
- 测试creators, producers, and mutators:调用observers来观察这些 operations的结果是否满足spec;
- 测试observers: 调用creators, producers, and mutators等方法产生或改变对象,来看结果是否正确。
## 表示独立性
- 表示独立性:client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应影响外部spec和客户端。
- 除非ADT的操作指明了具体的前置条件/后置条件,否则不能改变ADT的内部表示——spec规定了 client和implementer之间的契约。
【一个例子:字符串的不同表示】
让我们先来看看一个表示独立的例子,然后考虑为什么很有用,下面的MyString抽象类型是我们举出的例子。下面是规格说明:
1 /** MyString represents an immutable sequence of characters. */ 2 public class MyString { 3 4 //////////////////// Example of a creator operation /////////////// 5 /** @param b a boolean value 6 * @return string representation of b, either "true" or "false" */ 7 public static MyString valueOf(boolean b) { ... } 8 9 //////////////////// Examples of observer operations /////////////// 10 /** @return number of characters in this string */ 11 public int length() { ... } 12 13 /** @param i character position (requires 0 <= i < string length) 14 * @return character at position i */ 15 public char charAt(int i) { ... } 16 17 //////////////////// Example of a producer operation /////////////// 18 /** Get the substring between start (inclusive) and end (exclusive). 19 * @param start starting index 20 * @param end ending index. Requires 0 <= start <= end <= string length. 21 * @return string consisting of charAt(start)...charAt(end-1) */ 22 public MyString substring(int start, int end) { ... } 23 }
使用者只需要/只能知道类型的公共方法和规格说明。下面是如何声明内部表示的方法,作为类中的一个实例变量:
private char[] a;
使用这种表达方法,我们对操作的实现可能是这样的:
1 public static MyString valueOf(boolean b) { 2 MyString s = new MyString(); 3 s.a = b ? new char[] { 't', 'r', 'u', 'e' } 4 : new char[] { 'f', 'a', 'l', 's', 'e' }; 5 return s; 6 } 7 8 public int length() { 9 return a.length; 10 } 11 12 public char charAt(int i) { 13 return a[i]; 14 } 15 16 public MyString substring(int start, int end) { 17 MyString that = new MyString(); 18 that.a = new char[end - start]; 19 System.arraycopy(this.a, start, that.a, 0, end - start); 20 return that; 21 }
执行下列的代码
MyString s = MyString.valueOf(true); MyString t = s.substring(1,3);
我们用快照图展示了在使用者进行 subString 操作后的数据状态:
这种实现有一个性能上的问题,因为这个数据类型是不可变的,那么 substring 实际上没有必要真正去复制子字符串到⼀个新的数组中。它可以仅仅指向原来的 MyString 字符数组,并且记录当前的起始位置和终⽌位置。
为了优化,我们可以将这个类的内部表示改为:
private char[] a; private int start; private int end;
有了这个新的表示,操作现在可以这样实现:
1 public static MyString valueOf(boolean b) { 2 MyString s = new MyString(); 3 s.a = b ? new char[] { 't', 'r', 'u', 'e' } 4 : new char[] { 'f', 'a', 'l', 's', 'e' }; 5 s.start = 0; 6 s.end = s.a.length; 7 return s; 8 } 9 10 public int length() { 11 return end - start; 12 } 13 14 public char charAt(int i) { 15 return a[start + i]; 16 } 17 18 public MyString substring(int start, int end) { 19 MyString that = new MyString(); 20 that.a = this.a; 21 that.start = this.start + start; 22 that.end = this.start + end; 23 return that; 24 }
现在运行上面的调用代码,可用快照图重新进行 substring 操作后的数据状态:
由于 MyString 的使用者仅依赖于其公共方法和规格说明,而不依赖其私有的存储,因此我们可以在不检查和更改所有客户端代码的情况下进行更改。 这就是表示独立性的力量。
## ADT的特性:不变量(Invariants)与表示泄露
一个好的抽象数据类型的最重要的属性是它保持不变量。一旦一个不变类型的对象被创建,它总是代表一个不变的值。当一个ADT能够确保它内部的不变量恒定不变(不受使用者/外部影响),我们就说这个ADT保护/保留自己的不变量。
【一个栗子:表示泄露】
1 /** 2 * This immutable data type represents a tweet from Twitter. 3 */ 4 public class Tweet { 5 6 public String author; 7 public String text; 8 public Date timestamp; 9 10 /** 11 * Make a Tweet. 12 * @param author Twitter user who wrote the tweet 13 * @param text text of the tweet 14 * @param timestamp date/time when the tweet was sent 15 */ 16 public Tweet(String author, String text, Date timestamp) { 17 this.author = author; 18 this.text = text; 19 this.timestamp = timestamp; 20 } 21 }
我们如何保证这些Tweet对象是不可变的,(即一旦创建了Tweet,其author,message和 date 永远不会改变)对不可变性的第一个威胁来自客户实际上必须直接访问其领域的事实。