软件构造复习整理3

第4讲

静态与动态数据类型检查

静态类型语言:所有变量的类型在编译时已知,因此编译器可以推导表达式类型。在编译阶段进行类型检查。

动态类型语言:在运行阶段进行类型检查。

静态检查 >> 动态动态 >> 无检查

Static checking:可在编译阶段发现错误,避免了将错误 带入到运行阶段,可提高程序正确性/健壮性。语法错误,类名/函数名错误,参数数目错误,参数类型错误,返回值类型错误

Dynamic checking:非法的参数值,非法的返回值,越界,空指针

Mutability and Immutability

改变一个变量:将该变量指向另 一个存储空间。改变一个变量的值:将该变量 当前指向的存储空间中写入一个新的值。尽可能避免变化,以避免副作用

Immutability 不变性

不变性:重要设计原则。不变数据类型:一旦被创建,其值不能改变。如果是引用类型,也可以是不 变的:一旦确定其指向的对象,不能再被改变指向其他对象。

编译器进行静态类型检查时,如判断final变量首次赋值后发生了改 变,会提示错误

尽量使用final变 量作为方法的输入参数、作为局部变量。

Note:

–  final类无法派生 子类

– final变量无法改变值/引用

– final方法无法被子类重写

Advantage of mutable types:使用 不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收)。可变类型最 少化拷贝以提高效率。使用可变数据类型,可获得更好的性能。也适合于在多个模块之间共享数据。

既然如 此,为何还要用不可变类型?不可变类型更“安全”, 在其他质量指标上表现更好。可变性使得难以理解程序正在做什 么,更难满足方法的规约。折中,看你看重哪个质量指标。

如何修改代码?

通过防御式拷贝,给客户端返回一个全新的Date对象。大部分时候该拷贝不会被客户端修改, 可能造成大量的内存浪费。如果使用不可变类型, 则节省了频繁复制的代价。不可变类型不需要防御式拷贝。

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

作为代码级、运行时和时刻视图的快照图

快照图   用于描述程序运行时的内部状态。便于程序员之间的交流,便于刻画各类变量随时间变化,便于解释设计思路。

基本类型的值和对象类型的值

不可变对象:用双线椭圆:

不可变的引用:用双线箭头

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

可变的引用,也可指向不可变的值

Iterator as a mutable type 迭代器

迭代器是一个对象,它遍历一组元素并逐个返回元素

for(…:…)形式的遍历, 调用的是被遍历对象所实现的迭代器

------------------------------------------------------------------------------------------------------------------------

第5讲

设计规约

编程语言中的函数和方法

参数类型是否匹配,在静态类型检查阶段完成;返回值类型是否匹配,也在静态类型检查阶段完成

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

没规约,没法分派任务,无法写程序;即使写出来,也不 知道对错。契约: 程序与客户端之间达成的一致。Spec给“供需双 方”都确定了责任,在调用的时候双方都要遵守。

Why specifications?

Reality:很多bug来自于 双方之间的误解。不写下来,那么不同开发者的理 解就可能不同。没有规 约,难以定位错误。

Advantages:精确的规约,有助于区分责任。客户端无需阅读调用函数的代码,只需理解spec即可

行为等价性  这两个函数是否等价? 行为不同,但对用户来说 “是否等价”?根据规约 判断是否行为等价

前置/后置条件

前置条件:对客户端的约束,在使用方法时必须满足的条件;后置条件:对开发者的约束,方法结束时必须满足的条件。契约:如果前置条件满足了,后置条件必须满足

当前置条件被违反时,说明客户端有bug, 尽管实现者没有义务提醒,但可通过快速失败使bug更容易被找到和修复。

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

除 非在后置条件里声明过,否则方法内部不应该改变输入参数。应尽量遵循此规则,尽量不设计 mutating的spec,否则就容易引发bugs。程序员之间应达成的默契:除非spec必须如此,否则不 应修改输入参数。mutable对象会使规约复杂化。

程序中可能有很多变量指向同一个可变对象(别名)。无法强迫类的实现体和客户端不保存可变变量 的“别名”。不能靠程序员的“道德”,要靠严格的“契约”。

-------------------------------------------------------------------------------------------------------------------

第6讲

抽象数据类型 (ADT)

可变和不可变数据类型

可变类型的对象:提供了可改变其 内部数据的值的操作

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

对抽象类型的操作进行分类

构造器(从无到有);生产器(从有到新);观察器;变值器,改变对象属性的方法

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

变值器:通常返回 void;如果返回值为void,则必然意 味着它改变了对象的某些内部状态;变值器也可能返回非空类型

设计抽象类型

Rules of thumb 1 设计简洁、一致的操作:设计一组简单操作,通过简单操作的组合实现复杂的操作。操作的行为应该 是内聚的

Rules of thumb 2要足以支持client对数据所做的所有操作需要,且 用操作满足client需要的难度要低。判断方法:对象每个需要被访问到的属性是否都能够被访问到。没有get()操作就无法获取list的内部 数据。用遍历方式获取list的size –太复杂 vs 提供 size()操作,方便client使用。

表示独立性

表示独立性:client使用ADT时无需考虑其内部如何实 现,ADT内部表示的变化不应影响外部spec和客户端。

通过前提条件和后置条件充分刻画 了ADT的操作,spec规定了client和implementer之间的契约,明确 了client知道可以依赖哪些内容,implementer知道可以安全更改的内 容

为什么需要不变量:保持程序的“正确 性”,容易发现错误。如果没有这个不变量, 那么在所有使用String的地方,都要检查其是否改变了。

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

保持不变性和避免表示泄漏,是ADT最 重要的一个Invariant!

表示值构成的空间:实现者看到和使用的值

抽象值构成的空间:client看到和使用的值

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

满射,未必单射,未必单射

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

AF : R → A      AF:满射、非单射、 未必双射         R中的部分值并非合法的, 在A中无映射值

RI : R → boolean

▪表示不变性RI:某个具体的“表示”是否是“合法的”

▪ 也可将RI看作:所有表示值的一个子集,包含了所有合法的表示值

▪ 也可将RI看作:一个条件,描述了什么是“合法”的表示值       

同一个ADT,可以有多种表示;不同的内部表示,需要设计不同的AF和RI

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

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

即使是同样的R、同样的RI,也 可能有不同的AF,即“解释不同”。

设计ADT:(1) 选择R和A;(2) RI --- 合法的表示值; (3) 如何解释合法的表示值 ---映射AF 做出具体的解释:每个rep value如何映射到abstract value

而且要把这种选择和解释明确写到代码当中

Checking the Rep Invariant 随时检查RI是否满足

您当然应该调用 checkRep() 在每个创建或改变表示(创建者、生产者和变异者)的操作结束时断言表示不变量。在所有可能改变rep的方法内都要检查

----------------------------------------------------------------------------------------------------------------

第7讲

– Interface和Class: 定义和实现ADT

– 接口中只有方法的定义,没有实现

– 接口之间可以继承与扩展

– 一个类可以实现多个接口(从而具备了多个接口中的方法)

– 一个接口可以有多种实现类

接口中的每个 方法在所有类中都要实现,缺点是会导致部分方法的重复实现

通过default方法,在接口 中统一实现某些功能,无需在各个类中重复实现它

default 方法的典型使用方式:以增量式的为接口增加额外的功能而不破坏已实现的类

Inheritance and Overriding

严格继承:子类只能添加新方法,无法重写超类中 的方法

如果父类型中的某个函数实现体为空, 意味着其所有子类型都需要这个功能, 但各有差异,没有共性,在每个子类中 均需要重写

多态、子类型、重载

三种多态性

1.特殊多态:一个方法可以有多个同名的实现(方法重载)

2.参数化多态:一个类型名字可以代表多个类型(泛型编程)

3.子类型多态、包含多态:一个变量名字可以代表多个类的实例(子类型)

------------------------------------------------------------------------------------------------------------------

第8讲

ADT和OOP中的“等价性”

判定等价的三种方法

AF(a)=AF(b). 如果AF映射到同样的结果,则等价

站在外部观察者角度:对两个 对象调用任何相同的操作,都 会得到相同的结果,则认为这 两个对象是等价的。 反之亦然!

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

如果用==,是在判断两个对象身份 标识 ID是否相等(指向内存里的同一段空间)

Object.equals使用==实现 判断

应该总是使用 equals()判断相等(如 果判断逻辑是内存地址相等,则不需要重写Object.equals(),此时equals()等价 于==;如果判断逻辑是特定的,则需要重写Object.equals(),则equals()调用 的是期望的判断逻辑。无论哪种情况,使用equals()都能达到期望的目的)

equals()的自反、传递、对称

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

除非对象被修改了,否则调用多次equals应是同样的结果

“相等”的对象,其hashCode()的结果必须一 致

The hashCode contract

程序中多次调用同一对象的 hashCode方法,都要返回相同值

等价的对象必须有相同的hashCode

不相等的对象,也可以映射为同样的hashCode,但性能会变差

Overriding hashCode()

最简单方法:让所有对象的hashCode为同一 常量,符合contract,但降低了hashTable效率

通过 equals计算中用到的所有信息的hashCode组合出新的hashCode

可变对象的等价性

观察等价性:在不改变状态的情况下,两个mutable对象是否看起来一致

行为等价性:调用对象的任何方法都展示出一致的结果

对可变类型来说,往往倾向于实现严格的观察等价性

Java对其大部分可变数据类型(如 Collections)使用观察等价性,如两个List中包含相同顺序的元素, 则equals()返回true。部分可变类型用行为等价性。

但在有些时候,观察等价性可能导致bug,甚至可能破坏RI

对可变类型,实现行为等价性即可。也就是说,只有指 向同样内存空间的objects,才是相等的。所以对可变类型来说,无需重写这两个函数,直接继承 Object的两个方法即可。如果一定要判断两个可变 对象看起来是否一致,最好定义一个新的方法。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值