Chapter 3: Abstract Data Type (ADT) and Object-Oriented Programming (OOP)
introduction
这是在2019年六月复习软件构造的时候的整理笔记,不会只是简单的整理翻译课程PPT;对于简单的内容只是提提即可,对于实验中遇到的问题会做尽可能详细的说明。
3.1 Data Type and Type Checking
这一张主要介绍:
软件构造的理论基础——ADT
软件构造的技术基础——OOP
outline
- Data type in programming languages
- Static vs. dynamic data types
- Type checking
- Mutability & Immutability
- Snapshot diagram
- Complex data types: Arrays and Collections
- Useful immutable types
- Null references
- Summary
Data type
数据类型主要分为基本数据类型和对象数据类型
基本数据类型 | 对象数据类型 |
---|---|
int, long, byte, short, char, float, double, boolean | Classes, interfaces, arrays, enums, annotations |
No identity except their value | Have identity distinct from value |
Immutable | Some mutable, some not |
On stack, exist only when in use | On heap, garbage collected |
Can’t achieve unity of expression | Unity of expression with generics |
Dirt cheap | More costly |
对于对象类型,是具有层次结构的:由于java是面向对象的程序设计语言,一定会有继承的概念,自然也有父类和子类。
在java中默认所有对象都是Object的子类
这里需要说明的是虽然封装类型比如Integer,Boolean这些具有许多封装功能,但是由于是作为对象,需要占据大量内存,所以一般会推荐优先使用基本数据类型。
Static vs. dynamic data types
statically-typed language(静态类型语言):
在编译时进行类型检查 java
dynamically-typed languages (动态类型语言):
在运行时进行类型检查 python
Static Checking and Dynamic Checking
一般来说使用成熟的IDE(Eclipse,IDEA)都具有静态和动态类型检查;
static check:
– Syntax errors 语法错误, l
– Wrong names 类名/函数名错误
– Wrong number of arguments 参数数目错误
– Wrong argument types 参数类型错误
– Wrong return types 返回值类型错误,
dynamic check:
– Illegal argument values 非法的参数值.
– Unrepresentable return values 非法的返回值
– Out-of-range indexes 越界
– Calling a method on a null object reference. 空指针
综合的来看静态类型检查是关于“类型”的检查,动态类型检查是关于“值”的检查
Mutability and Immutability
首先要明确改变一个变量和改变一个变量的值得区别:
改变一个变量:将该变 量指向另一个值的存储空间
改变一个变量的值:将 该变量当前指向的值的存储空间中写入一个新的值。
Immutable data type :
不可变数据类型,一旦被创建,其值无法改变,但是其引用是可以改变的,e.g. String
如果想要该对象引用不可变(make reference immutable):使用final 关键词
final:
– A final class declaration means it cannot be inherited.
final类无法派生子类
– A final variable means it always contains the same value/reference but cannot be changed
final变量无法改变值/引用
– A final method means it cannot be overridden by subclasses
final方法无法被子类重写
Objects are immutable: once created, they always represent the same value. e.g. String
Objects are mutable: they have methods that change the value of the object. e.g. StringBuilder
使用不可变对象:对其频繁修改会产生大量临时拷贝,需要垃圾回收;更加安全
使用可变对象:减少拷贝,提高效率;可以作为共享数据为多个模块之间共享;性能更高;安全性不受保障
使用可变对象的危险来源:
作为参数传递
作为返回值返回
存在多个别名(这样一旦mutable对象被修改,很难定位修改该对象的别名(引用))
使用可变对象的安全措施:
defensive copy
有以下两种措施;:
new List<>(source)
Collections.unmodifiableList(source)
Snapshot diagram as a code-level, run-time, and moment view
通过snapshot的方式给出程序运行快照,便于程序员交流解释思路;下图中左侧代表的是不可变对象String 从 “a”变为“ab”的过程;
右侧是可变对象Stringbuilder从“a”变成“ab”的过程,能够非常清晰看清楚区别:
可变的引用:单线箭头
可变的对象:单线椭圆
不可变引用:双线箭头
不可变对象:双线椭圆
Complex data types: Arrays and Collections
Arrays:
数组,我们在C中就已经遇到过的数据结构,非常有用,但是是定长的,需要提前申请好,对于一开始不知道长度的数据而言,数组显得比较笨拙;但是数据的效率很高。
Collections:
java中提供了以下几种常用的集合类,能够有效地支持编程:
List,Set,Map
注意以上这些集合类都是抽象接口,定义提供方法,但是没有具体实现。我们要用Concrete Class去实现(implements)这些接口;这样的好处在于信息影藏,user只需要知道能够使用哪些方法,而不需要去知道内部的实现。
这里还介绍了对于集合类中这些集合数据中一种新的遍历方式iterator(迭代器)
实际上在后面设计模式章节的时候我们会谈到如何实现迭代器模式,但是在这里还是简答的提一下:
iterator中含有两个方法:
next(); mutator methods
hasNext();
本来我还有疑问,为什么next是mutator方法,直到看到了next()具体实现:
之前在使用Iterator强调不可以一边遍历一边删除,但是这里提供了新方法(一种可以,一种不行):
首先初始化 monkey=[1,2,3,4,3,5,3],我现在希望删掉所有等于3数字,之前使用的方法是:
程序运行结果:
新方法:
运行结果:
Useful immutable types
基本类型及其封装类型都是不可变的
List, Set, Map 都是可变的
java中还提供了了上述集合类的不可变封装,即将集合类的所有mutator 方法全部阻塞。但是这种阻塞只在运行时才能发现,在静态编译时发现不了,所以对一个unmodifiableList,在code时仍然可以调用sort()方法,但是会在运行时得到Runtime Exception
3.2 Designing Specification
这一小节主要介绍“方法/函数/操作”如何定义—— 编程中的“动词”、规约;实际上在做完六个实验之后,我深刻的理解到了这一部分或许才是软件开发中最为重要的一部分:设计出好的ADT结构和Spec,能够极大程度提高程序的可观赏性,可扩展性等等。
总之Spec是一个非常重要的东西,他能够让你的代码真正体现出价值。
Behavioral equivalence
通过Spec,开发者和用户之间就拥有了一个很好的沟通桥梁。用户可以通过Spec知道程序能够做什么,而不需要开发者告诉他具体的实现;开发者可以在spec的掩护下任意修改实现方式,只需要维护程序外观一致即可。
但下面就遇到这样一个问题:如何判别多种实现是否具有一样的外部行为呢?
Answer:需要从客户端和Spec的角度衡量
Specification structure: pre-condition and post-condition
简单的而言前置条件就是输入参数需要满足的要求,后置条件是方法接受参数处理后的返回值需要满足的要求。
应该尽量避免设计mutator 的spec,根据传统,除非特殊要求,不应该对传入参数做修改
可变数据类型会使得Spec非常难以设计
Testing and verifying specifications
Black-box testing
从spec的角度出发,设计测试(把自己想象成于开发无关,不知道内部实现的人)
example:
Designing specifications
设计Spec ,实际上需要设计前置后置条件,这里还涉到Spec的替换。一般而言,更宽泛的precondition 和 更严格的 postcondition 是强度更大的规约,我们可以用强度大的Spec替换强度小的;
Spec的类型:
操作式:以伪代码的格式给出(不推荐,内部实现不应暴露)
声明式:只有“初-终”状态(更加清晰,更有价值)
实现方式应该在函数体内部以注释形式给出
是否应该检查参数合法性:
用户不喜欢太强的限制,应该设计尽可能宽泛的precondition,如果参数异常在后置条件以异常方式报错;
归纳:是否使用前置条件取决于(1) check的代价;(2) 方法的使用范围 – 如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用 该方法的各个位置进行check——责任交给内部client; – 如果在其他地方使用该方法(public),那么必须要使用前置条件,若client端 不满足则方法抛出异常。
3.3 Abstract Data Type (ADT)
3-1节研究了“数据类型”及其特性
3-2节研究了方法和操作的“规约”及其特性
这一节主要讲述如何从Abstract Function,Rep Invariant, Safety from exposure 设计出好的,可复用的ADT
Abstract Data Type 抽象数据类型和这些关键词都有关系:
信息隐藏,封装, 模块化,抽象;
抽象数据类型的数据抽象过程是建立在由一组操作刻画的数据类型上,这一点和数据结构中我们学习的有点区别,但实际上是侧重点不一样,这里是指作为用户只需要接触到操作即可,不需要了解数据的具体表示和储存方式,但实际上程序员还是要考虑这一点的,但是你设计的抽象数据接口不需要体现这一点,只需要提供你希望暴露给用户的操作即可。
以我们在软件构造实验二为例,我们设计了一个Graph泛型ADT,我们只有source(),target(),这些方法;但是Graph是以Edge为基础还是以Vertex为基础储蓄的用户并不需要知道;这些是由程序员在Concrete Class 中实现即可。
an abstract data type is defined by its operations.
ADT上方法的类型:
可变的ADT有mutator 方法,不可变的ADT没有mutator 方法。
Designing an Abstract Type
– Rules of thumb 1 设计简洁、一致的操作
– Rules of thumb 2 要足以支持client对数据所做的所有操作需要,且 用操作满足client需要的难度要低
– Rules of thumb 3 要么抽象、要么具体,不要混合 — 要么针对抽象 设计,要么针对具体应用的设计
Representation Independence(表示独立性)
即用户不应该知道其内部数据表示:
保持RI的ADT的例子(这个例子也不好,fields用public修饰,用户可直接访问,依然可以知道该ADT内部表示):
Java中实现ADT的方式:
Testing an Abstract Data Type
– 测试creators, producers, and mutators:调用observers来观察这些 operations的结果是否满足spec;
– 测试observers:调用creators, producers, and mutators等方法产生或 改变对象,来看结果是否正确。
写测试策略的例子:
Invariants
一个好的ADT必须要维护自己的不变性,不变性能够便于我们debug,查找出代码中的错误;为了达到这一点,我们总结出以下建议:
- 不要将可变数据类型引入到ADT中,如Date之类的;尽量不要让fields中出现可变对象
- 如果必须要使用mutable data type ,注意在构造和返回的时候使用防御式拷贝技术
Rep Invariant and Abstraction Function
这是两个非常抽象的概念,但是也是ADT最为核心的东西;
假设R,A是两个空间,R是表示空间,A是抽象空间
程序员主要关注R,这是ADT内部的表示方式,用户关注A,这是ADT的外部表示;
AF抽象函数即是R到A的映射关系,注意这里AF是满射,但是未必单射,未必双射。
如果AF不是双射,说明R中不是所有的元素都能映射到A中的元素,即我们设计的ADT中所利用的R’只是R的子集;为了将这种关系清晰描绘,我们需要RI,说明R中合法元素的取值条件;
对于一个ADT,我们首先设计在这个数据类型上的操作,接下来我们就需要设计其内部表示;而这个过程实际上就是我们要设计不同的AF和RI
综上:我们得出设计ADT的一般步骤:
(1) 选择R和A;
(2) RI — 合法的表示值;
(3) 如何解释合法的表示值 —映射AF 做出具体的解释:每个rep value如何映射到abstract value
要把这种选择和解释明确写到代码当中
Example1:
Example2:
用ADT的表示不变性取代复杂的前置条件,相当于封装;
注意AF,RI这些都属于内部表示,所以要以注释的形式注释在代码中,而不能写在javadoc中。