软件构造课复习笔记【3】

3-1数据类型与类型检验

1编程语言中的数据类型
2.静态和动态数据类型检查
3.可变性和不变性
4.快照图
5.复杂数据类型:数组和集合
6.有用的不可变类型

编程语言中的数据类型(Java)

基本数据类型:int, long, boolean, double, char
不可变的
只有值,没有ID (与其他值无法区分)
在栈中分配内存
代价低
对象数据类型:String, BigInteger
可变/不可变都有
既有ID,也有值
在堆中分配内存
代价昂贵

对象类型形成层次结构

根节点是Object类。
其他所有类都是继承Object类而来。
一个类是其所有父类的实例。

包装类型

将基本类型包装为不可变对象类型
通常是在定义容器类型的时候使用它们
会降低性能
一般可以自动转换
例:list.add(1) ; 等价于list.add(Integer.valueOf(1));

重载(Overloading)

同样的操作名可用于不同 的数据类型
操作符,方法都可以被重载

静态、动态数据类型检查

Java是静态类型语言(在编译阶段进行类型检查 )
动态类型语言在运行阶段进行类型检查

发现错误的能力:静态检查 >> 动态动态 >> 无检查

静态检查:可在编译阶段发现错误,避免了将错误带入到运行阶段,可提高程序正确性/健壮性
静态检查包括:

语法错误
类名/函数名错误
参数数目错误
参数类型错误
返回值类型错误

动态检查包括:

非法的参数值
非法的返回值
越界
空指针

静态检查:关于“类型”的检查,不考虑值
动态检查:关于“值”的检查
注:the result of Math.sqrt(a) is NaN, not a dynamic error! (浮点数中有Nan表示非零数)

可变和不可变(Mutability and Immutability)

改变一个变量:将该变量指向另一个值的存储空间
改变一个变量的值:将该变量当前指向的值的存储空间中写入一个新的值

尽可能避免变化,以避免副作用

不变性:重要设计原则
不变数据类型:一旦被创建,其值不能改变。如果是引用类型,也可以是不变的:一旦确定其指向的对象,不能再被改变指向其他对象
使用final修饰词来创建一个不可变对象。
如果编译器无法确定final变量不会改变,就提示错误,这也是静态 类型检查的一部分。

final类无法派生子类
final变量无法改变值/引用
final方法 无法被子类重写

不变对象:一旦被创建,始终指向同一个值/引用
可变对象:拥有方法可以修改自己的值/引用

不可变对象以string为例,当string改变时,实际上是创建一个新的string并指向他,而把旧的string删掉。也因此相同内容的string可以共用一块内存存储内容。
相对的,StringBuilder是可变类型。因此有多个引用指向一块内存时,改变一个其他所有引用的内容都会改变。

可变与不可变类型的优缺点:
使用不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收) 
不可变类型更“安全”, 在其他质量指标上表现更好 

可变性使得难以理解程序正在做什 么,更难满足方法的规约。
可变类型最少化拷贝以提高效率 
使用可变数据类型,可获得更好的性能 ,也适合于在多个模块之间共享数据 
解决可变类型的缺点

通过防御式拷贝,给客户端返回一个全新的Date对象。

大部分时候该拷贝不会被客户端修改,可能造成大量的内存浪费
不可变类型不需要防御式拷贝

安全的使用可变类型:局部变量,不会涉及共享;只有一个引用
如果有多个引用(别名),使用可变类型就非常不安全

快照图(Snapshot diagram)

可以表示代码的运行时时间片状态。
用于描述程序运行时的内部状态
作用:

便于程序员之间的交流 
便于刻画各类变量随时间变化
便于解释设计思路 
表达方法

基本类型的值:箭头

对象类型的值:箭头+圆圈

不可变对象:双线圆圈(变化:在原对象画叉并指向新的)

可变对象变化:在内容中画叉

不可变的引用(如final修饰):双线箭头

引用是不可变的,但指向的值却可以是可变的
可变的引用,也可指向不可变的值

复合数据类型:Arrays 和 Collections

定长数组不可改变长度,List, Set, Map长度可变

List有顺序,Set无序且内容不可重复,Map为一系列键值对,其中键无序且不可重复
当添加一个item时 ,编译器执行静态检查,确保只添加合适类型的item。
不能创建基础类型的 collection ,只能创建对象类型的 collection ,也是包装类型的作用之一。

Iterator迭代器

迭代器是一个对象,它遍历一组 元素并逐个返回元素。
迭代器以for(…:…)形式的遍历, 调用的是被遍历对象所实现的迭代器

空引用 (NULL)

基本数据类型不能为空。
字符串的null与“”不同。
非空的collection可以包含null值

3-2设计规约(Designing Specification )

“方法”是程序的“积木”,可以被独立开发、测试、复用
使用“方法”的客户端,无需了解方法内部具体如何工作—“抽象”

Java文档

会由你的spec自动生成函数的说明,并且支持查看,导出等操作

文档中包括:
类的层次体系
类的介绍
构造器的介绍
方法的摘要和可用方法

代码中蕴 含的“设计决策”:给编译器读
注释形式的“设计决策”:给自己和别人读

Specification规约

规约是程序与客户端之间达成的一致
Spec给“供需双 方”都确定了责任,在调用的时候双方都要遵守
客户端无需阅读调用函数的代码,只需理解spec即可,解耦
规约可以隔离“变化”,无需通知客户端

规约内容包括:

输入/输出的数据类型 
功能和正确性 
性能

(只讲“能做什么”,不讲 “怎么实现”)

行为等价性,即两个方法达成的功能是否完全一致,要站在客户端视角看
可以根据规约 判断是否行为等价,这两个函数符合这个规约,故它们等价。

前置条件:对客户端的约束,在使用方法时必须满足的条件
后置条件:对开发者的约束,方法结束时必须满足的条件
契约:如果前置条件满足了,后置条件必须满足
前置条件不满足,则方法可做任何事情。

静态类型声明是一种规约,可据此进行 静态类型检查static checking。
方法前的注释也是一种规约,但需人工判定其是否满足

spec的标准写法:

除非在后置条件里声明过,否则方法内部不应该改变输入参数
方法中尽量避免使用mutable的对象,不能依赖双方的道德来约束不可变内容,应该从设计上,在规约中就限定住不应改变的对象无法被改变。

测试和验证的spec

黑盒测试:依据规约设计测试用例,不考虑实现,同其他client一样

设计规约(Designing specifications)

如何比 较两个规约,以判断是否可以用一个规约替换另一个?
规约的强度S2>=S1,可以用S2替代S1 :

前置条件更弱 
后置条件更强 

越强的规约,意味着implementor的自由度和责任越重,而client的 责任越轻。

确定的规约(Deterministic):给定一个满足precondition的输入,其输出是唯一的、明确的
欠定的规约(Under-deterministic):同一个输入可以有多个输出
非确定的规约(Nondeterministic):同一个输入, 多次执行时得到的输出可能不同

确定的 优于 欠定的 优于 非确定的规约

操作式规约,例 如:伪代码
声明式规约:没有内部实现的描述,只有 “初-终”状态
声明式规约更有价值
内部实现的细节不在规约里呈现,放在 代码实现体内部注释里呈现。

良好规约的设计特点

1.内聚的:Spec描述的功能应单一、简单、易理解
2.信息丰富的:不能让客户端产生理解的歧义
3.足够强壮:开发者应尽可能考虑各种特殊情况,在post-condition给出处理措施
4.同时足够自由:太强的spec,在很多特殊情况下难以达到,给开发者增加了实现的难度
5.使用抽象类型: 在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度
6.前置条件的检查:不写Precondition,就要在代码内部check;若代价太大, 在规约里加入precondition, 把责任交给client

客户端不喜欢太强的 precondition,不满足precondition的输入会导致失败
惯用做法是: 不限定太强的precondition,而是在postcondition中抛出异常:输入不合法
尽可能在错误的根源处fail,避免其大规模扩散

3-3抽象数据类型(ADT)

抽象数据类型与表示独立性:能够分离程序中数据结构的形式和对其使用的方式
抽象数据类型与表示独立性:如何设计良好的抽象数据结构,通过封 装来避免客户端获取数据的内部表示(即“表示泄露”),避免潜在 的bug——在client和implementer之间建立“防火墙”

ADT的特性:不变量、表示泄漏、抽象函数AF、表示不变量RI

抽象数据类型和自定义数据类型

传统的类型定义:关注数据 的具体表示
抽象类型:强调“作用于数据上的操作”,程序员和 client无需关心数据如何具体存储的,只需设计/使用操作即可。

ADT是由操作定义的,与其内部如何实现无关!

数据类型和操作符的分类

数据类型:可变和不可变

可变类型的对象:提供了可改变其 内部数据的值的操作 
不可变数据类型: 其操作不可 改变内部值,而是构造新的对象 

抽象操作的分类:

构造器(Creators):从无到有
生产器(Producers):从旧对象创建新对象
观察器 (Observers):获得变量的值
变值器(Mutators):改变对象属性的方法

构造器:可能实现为构造函数或静态函数
实现为静态方法的构造器通常称为工厂方法

变值器通常返回 void
如果返回值为void,则必然意 味着它改变了对象的某些内部状态

设计抽象类型

良好ADT的设计:靠“经验 法则”,提供一组操作,设计其行为规约 spec
要点1:设计简洁、一致的操作
要点2:要足以支持client对数据所做的所有操作需要,且 用操作满足client需要的难度要低
判断方法:对象每个需要被访问到的属性是否都能够被访问到
要点3:要么抽象、要么具体,不要混合 — 要么针对抽象 设计,要么针对具体应用的设计

表示独立性(Representation Independence)

表示独立性:client使用ADT时无需考虑其内部如何实 现,ADT内部表示的变化不应影响外部spec和客户端。
spec规定了client和implementer之间的契约,明确 了client知道可以依赖哪些内容,implementer知道可以安全更改的内容。

如何维持RI:作为设计者必须遵守自己定下的spec,当内部代码出现变化时不应引起规约中约定的使用方法的变化,如,若内部变量类型变化了,内部应拷贝一份变成spec中声明的类型返回。

测试抽象数据类型

测试creators, producers, and mutators:调用observers来观察这些 operations的结果是否满足spec;
测试observers:调用creators, producers, and mutators等方法产生或 改变对象,来看结果是否正确

不变量Invariants

ADT需要始终保持其不变量
由ADT来负责其不变量,与client端的任何行为无关
为什么需要不变量:保持程序的“正确性”,容易发现错误
总是要假设client 有“恶意”破坏ADT不变量的行为—防御式编程

表示泄露:不仅影响不变量,也影响了表示独立性:无法在不影响客户端的情况下改变其内部表示

保持表示不变量,在交出可变的变量时交出拷贝,使得交出的变量的恶意/非恶意改变不会影响内部代码
也可以在spec中声明,但由此引发的潜在bug也将很多。当复制代价很高时,不得不这么做。

除非迫不得已,否则不要把希望寄托于客户端上,ADT有责任保证自 己的invariants,并避免“表示泄露”。最好的办法就是使 用immutable的类型。

表示不变量和抽象函数 Representation Invariant and Abstraction Function

R:表示值构成的空间:实现者看到和使用的值
A:抽象值构成的空间:client看到和使用的值

ADT开 发者关注表示空间R,client关注抽象空间A

对于R和A的关系:满射,未必单射,未必双射。

抽象函数(AF):R和A之间映射关系的函数,即如何将R中的每一个值解释为A中的每一个值。

表示不变性RI:某个具体的“表示”是否是“合法的”
也可将RI看作:所有表示值的一个子集,包含了所有合法的表示值
也可将RI看作:一个条件,描述了什么是“合法”的表示值
例:

选择某种特定的表示方式R,进而指定某个子集是“合 法”的(RI),并为该子集中的每个值做出“解释”(AF)——即如何映射 到抽象空间中的值。

同样的表示空间R,可以有不同的RI
同样的R、同样的RI,可能有不同的AF

设计ADT:

(1) 选择R和A;
(2) RI --- 合法的表示值; 
(3) 如何解释合法的表示值 ---映射AF 

Check Rep:(随时检查RI是否满足)
在所有可能改变rep的方法内都要检查,rep是否保持了

有益的可变性 Beneficent mutation

对immutable的ADT来说,它在A空间的abstract value应是不变的。
但其内部表示的R空间中的取值则可以是变化的。

mutation只是改变了R值,并未改变A值,对客户端来说依然是immutable的,保证内部变化不会引起客户端失效,同时内部的mutable便于内部编写并提高了效率。(通过牺牲immutability的部分原则来换取“效率”和“性能”)

用文档记录AF, RI, 并且没有表示泄露

要精确的记录RI:rep中的所有fields何为有效
要精确记录AF:如何解释每一个R值
给出理由,证明代码并未对外泄露其内部表示——自证清白

ADT的规约里只能使用client可见的内容来撰写,包括参数、返 回值、异常等。 (否则在注释中就发生泄露)
ADT的规约里也不应谈及任何内部表示 的细节,以及R空间中的任何值
ADT的内部表示(私有属性)对外部都应严格不可见
故在代码中以注释的形式写出AF和RI而不 能在Javadoc文档中,防止被外部看到而破坏表示独立性/信息隐藏

构造器和生产器在创建对象时要确保不变量为true
变值器和观察器在执 行时必须保持不变性。
在每个方法return之前,用checkRep()检查不变量是否得以保持。

ADT不变量取代前置条件

用ADT不变量取代复杂的 Precondition,相当于将复杂的precondition封装到了ADT内部。
减少client需要靠道德维持的操作,而在内部使用代码规定。

3-4面向对象的编程(OOP)

OOP中的技术:

封装与信息隐藏 
继承与重写 
多态、子类型、重载 
静态与动态分派 
基本概念:对象、类、属性、方法

对象是状态和方法的集合体
每个对象都有一个类。类同时定义类型和实现。
类中包括:类成员变量和类方法。
在类定义时创建的是静态变量/方法,随对象创建的是实例变量/方法。
静态方法不能调用非静态成员。

接口和枚举类

接口中只有方法的定义,没有实现
接口之间可以继承与扩展
一个类可以实现多个接口(从而具备了多个接口中的方法)
一个接口可以有多种实现类

接口:确定ADT规约;类:实现ADT
可以不需要接口直接使用类作为ADT,既有ADT定义也有ADT实现

不同的需求,不同的行为导致需要多种不同的实现,就需要用同一种接口完成不同的实现。

枚举类:类中只有一系列枚举变量,对于该类的一个实例,不允许出现列出的变量以外的值。

封装与信息隐藏

内部数据和实现细节的隐藏程度是模块化设计质量评价的最重要标准
API同实现分离
模块间只通过API通讯

使用接口类型声明变量
客户端仅使用接口中定义的方法
客户端代码无法直接访问属性

继承和重写(Inheritance and Overriding)

严格继承:子类只能添加新方法,无法重写超类中的方法
final关键字修饰:

修饰字段:防止初始化后重新分配该字段
修饰方法:防止重写该方法
修饰类:防止继承类

重写的函数:完全同样的signature
实际执行时调用哪个方法,运行时决定。

父类型中的被重写函数体不为空:意味着对其大多数子类型来说,该方法 是可以被直接复用的。对某些子类型来说,有特殊性,故重写父类型中的函数,实现自己的特殊要求。
如果父类型中的某个函数实现体为空,意味着其所有子类型都需要这个功能,但各有差异,没有共性,在每个子类中均需要重写。

重写之后,利用super()复用了父类 型中函数的功能,并对其进行了扩展
重写的时候,不要改变原方法的本意

使用 @Override 来声明一个方法重写,签名要保持一致

抽象类

只有定义没有实现
抽象类不能实例化(不能用new 生成对象)
继承某个抽象类的子类在实例化时,所有父类中的抽象方法必须已经实现

如果某些操作是所有子类型都共有, 但彼此有差别,可以在父类型中设计 抽象方法,在各子类型中重写
所有子类型完全相同的操作, 放在父类型中实现,子类型中 无需重写。
有些子类型有而其他子类型无 的操作,不要在父类型中定义 和实现,而应在特定子类型中 实现。

多态、子类型、重载

多态的分类:
特殊多态:一个方法可以有多个同名的实现(方法重载)
参数化多态:一个类型名字可以代表多个类型(泛型编程)
子类型多态:一个变量名字可以代表多个类的实例(子类型)

重载:多个方法具有同样的名字,但有不同的参数列表或返回值类型
价值:方便client调用,client可用不同的参数列表,调用同样的函数
重载也可以发生在父类和子类之间

参数多态性和泛型

泛型编程围绕“从具体进行抽象”的思想,将采用不同数据表示的算法进行抽象,得到泛型化的算法,可以得到复用性、通用性更强的软件。

使用泛型变量的三种形式:泛型类、泛型接口和泛型方法
类中如果声明了一个或多个泛型变量,则为泛型类
这些类型变量称为类的类型参数
例:

泛型接口,非泛型的实现类:

泛型接口,泛型的实现类 :

多态

一个类只有一个父类,但可以实现多个接口
子类型的规约不能弱化超类型的规约。
子类型多态:不同类型的对象可以统一的处理而无需区分

3-5ADT和OOP中的“等价性”

等价关系

现实中的每个对象实体都是独特的, 所以无法完全相等,但有“相似性”

等价关系:自反、对称、传递

判断等价的方法

如果AF映射到同样的结果,则等价
站在外部观察者角度:对两个对象调用任何相同的操作,都会得到相同的结果,则认为这两个对象是等价的。

“==” 与 equals()

==:引用等价性,二者指的是同一个地址空间
equals() :对象等价性 ,一般是二者的内容相等

在自定义ADT时,需要根据对“等价”的要求,决定是否重写Object的equals()

对基本数据类型,使用==判定相等
对对象类型,使用equals()

实现equals()方法

在Object中实现的缺省equals()是在判断引用等价性,因此在自定义的ADT中一般需要重写equals()方法。
利用@Override声明重写,让编译器确保签名的正确性
重写时参数应为object类型以保证参数类型相同,然后在方法内部进行类型转换。

instanceof:判断某个对象是不是特定类型(或其子类型) ,动态类型检查,不是静态类型检查
除了用于实现equals()方法,尽可能避免使用 instanceof和getClass() !
例:

equals()方法应满足:

自反性 
对称性 
传递性 
一致性(用来判断 等价的信息不发生变化时,多次比较的结果不变) 
对类的所有对象都生效

哈希表实现了键-值之间的映射
程序中多次调用同一对象的hashCode方法,都要返回相同值,但不要求程序的多次执行时相同
等价的对象(两个equal的objects)必须有相同的hashCode
一般做法:通过equals计算中用到的所有信息的hashCode组合出新的hashCode

可变类型等价性

观察等价性:在不改变状态的情况下,两个mutable对象是否看起来一致
行为等价性:调用对象的任何方法都展示出一致的结果

对可变类型来说,往往倾向于实现严格的观察等价性
但在有些时候,观察等价性可能导致bug,甚至可能破坏RI

注: 如果某个mutable的对象包含在Set集 合类中,当其发生改变后,集合类的行为不确定 ,务必小心

自动包装和等价性

例:
放入Map的时候, 自动将int转为了Integer
取出来的时候,得到的是Integer类型
所以map中存入两个相同数字再取出不一定相等。
由于Java的缓存机制,数字在-128到127之间采用同一块内存,“==”判断是相等的,超出范围判断是不等的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值