第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的两个方法即可。如果一定要判断两个可变 对象看起来是否一致,最好定义一个新的方法。