软件构造第二部分——ADT+OOP内容梳理
一.数据类型与数据检验
1.编程语言的数据类型:和计算机体系结构有关
变量:用特定数据类型定义,可存储满足类型约束的值。
在Java中,有基本数据类型(primitive types),例如:int,long,Boolean,double,char,以及对象数据类型(object types),例如:String,BigInteger。

对象类型形成层次结构,以单亲继承关系继承。继承关系:从其超类继承可见字段和方法,可以重写方法以更改其行为。
可以将基本类型包装成对象类型(对应大写),通常在定义集合类型时使用它们,一般情况下尽量避免使用。
操作:给输入产生输出或改变操作值本身的函数。
重载:同样的操作名可以用于不同的数据类型。
2.静态/动态数据检验
Java是静态类型语言,所有变量的类型在编译时就是已知的,因此编译器也可以推断所有表达式的类型,在编译阶段就进行类型检查。(最好)
动态类型语言(Python):在运行阶段进行类型检查。(较好)
无检查(C语言),不检查数组越界。(不好)
静态检查的错误类型包括:语法错误,类名/函数名错误,参数数目错误,参数类型错误,返回值类型错误等。
动态检查的错误类型包括:非法的参数值,非法的返回值,越界,空指针等。
静态/动态检查的区别:静态类型保证变量将具有该集合中的某些值,但直到运行时我们才知道具有的确切值。因此,如果错误只由某些值(被零除/索引超过范围)引起,那么编译器将不会提出静态错误。
静态检查只查类型,不考虑值;动态检查查值。
3.可变性和不可变性(Mutability and Immutability)
赋值:存储的过程,用“=”将计算结果放到内存中。
改变一个变量:将该变量指向另一个值的存储空间。
改变一个变量的值:将该变量当前指向的值的存储空间中写入一个新的值。
不变性:重要设计原则,一个不变数据类型一旦被创建,其值不能改变;如果是引用类型(指针),也可以是不变的:一旦确定其指向对象,就不能再被改变。
用final保证变量不可变,如果编译器无法确定final可不可变就报错,是静态类型检查的一部分。
尽量使用final作为方法的输入参数,以及局部变量。
注意:final类无法派生子类,final变量无法改变值/引用,final方法无法被子类重写。


其中String为不可变类型,StringBuilder为可变类型,用代码快照图理解如上。
当有多个引用时,两者就会有区别了:

执行之后,s仍然是ab,而sb和tb一样被改为了abc。
因此,使用不可变类型的频繁修改会产生大量的临时拷贝(需要垃圾回收);可变类型可以达到最小化拷贝以提高效率。使用可变数据类型可以获得更好的性能,也适用于在多个模块之间共享数据,但相当于全局变量。但不可变类型更安全,在其他质量指标上表现更好。
防御式拷贝:返回新建的对象,即返回副本而不是原来的值。
可变类型只在局部变量,不会涉及多个共享;只有一个引用时好用,有多个引用时使用可变类型就很不安全。
4. Snapshot diagram(代码快照图):as a code-level, run-time, and moment view
用于描述程序运行时的内部状态:包括栈(程序的方法和本地变量)和堆(存在的对象)。
作用:便于程序员之间的交流,便于刻画各类变量随时间变化,便于解释设计思路。

对于可变对象,用单线椭圆即可;对于不可变对象,要用双线椭圆:

对于不可变的引用,要用双线箭头:

引用不可变,但指向引用的值是可变的;可变的引用也可以指向不可变的值。
5.复杂的数据类型:Arrays and Collections
Array:fixed-length sequences of another type T。(数组)
List:variable-length sequences of another type T。(链表)(接口)
Iterating:取Array或者List的最大值。(方法)
Set:an unordered collection of zero or more unique objects。(集合)(抽象接口)
Map:类似字典(存储键值对)。(抽象接口)
使用Java集合时,我们可以限制集合中包含的对象的类型;当我们添加项时,编译器可以执行静态检查,以确保我们只添加适当类型的项;然后取出时可以保证取出的类型。
对接口的实现方法:List:ArrayList and LinkedList;Set:HashSet;Map:HashMap。
Iterator:迭代器,是一个逐步遍历元素集合并逐个返回元素的对象。(无序变有序),包括next(返回下一个集合的元素)方法(是可变的)和hasNext(检查迭代器是否返回集合中最后一个元素)方法。
6.有用的不可变类型:基本类型及其封装对象类型都是不可变的。
Data是可变类型,所以用java.time代替。
集合类型都是可变的,可以用Collections.unmodifiableList/Set/Map代替,得到的结果是不可变的,只能看不能改,但是是在运行阶段获得的,编译阶段无法进行静态检查。
那么为了创造不可变聚合类型,我们需要使用List/Set/Map.of新建,然后用copyof创建这些不可变类型的可变副本。、
二.设计规约
1.编程语言的函数/方法
方法是程序的积木,可以被独立开发,测试,复用;使用方法的客户端,无需了解方法内部如何具体工作——是一种抽象。
2.Specification(规约): Programming for communication
(1)Documenting in programming
假设:变量的数据类型定义,final表示设计决策不可改变,是一种决策。
代码中的设计决策给编译器读;注释的设计决策给程序员读。
(2)Specification and Contract (of a method)
规约是团队工作的关键,是程序与客户端达成的一个契约,spec给供需双方都确定了责任,在调用的时候双方都要遵守。
规约内容:输入输出的数据类型,功能和正确性,性能。
(3)Behavioral equivalence(行为等价性)
站在客户端根据规约判断两个函数行为是否等价。
(4)Specification structure:pre-condition and post-condition
前置条件:以require开头,对客户端的约束,在使用方法时必须满足的条件。
后置条件:以effects开头,对开发者的约束,方法结束时必须满足的条件。
契约:如果前置条件满足了,后置条件必须满足。
静态类型声明是一种规约,可以据此进行静态类型检查;方法前的注释也是一种规约,但需要人工判定其是否满足。
Spec只讨论方法的参数和返回值,不讨论方法的类的局部变量或私有域。
尽量不对可变类型方法写spec,也不应该修改输入参数。
3. Designing specification(设计规约)
(1)Classifying specification
规约的确定性;陈述性;强度,用于判断哪个规约更好。
强度比较:前置条件更弱,后置条件更强的规约更好,有一个不满足的话无法比较。
确定的规约:给定一个满足前置条件的输入可以有唯一输出。
欠定的规约:同一个输入可以有多个输出。(通常有确定的实现)
非确定的规约:同一个输入,多次执行时得到的输出可能不同。
(2)Diagramming specification
对于某个具体实现,若满足规约,则落在其范围内;否则,在其之外。程序员可以在规约的范围内自由选择实现方式,客户端无需了解具体使用哪个实现。
更强的后置条件和更弱的前置条件意味着区域更小。
(3)Designing good specifications
Spec描述的功能应该单一,简单,易理解;信息丰富;不弱不强;在规约里使用抽象类型,可以给方法的实现体与客户端更大的自由度;前置条件交给客户验证。
如果只在类的内部使用该方法(private),可以不使用前置条件,在方法的内部进行检查;在其他地方使用该方法(public),那必须给出前置条件,不满足应抛出异常。
三.抽象数据类型(ADT)
1. Abstraction and User-Defined Types
除了编程语言所提供的基本数据类型和对象数据类型以外,还可定义自己的数据类型。
数据抽象:由一组操作所刻画的数据类型。抽象类型强调作用于数据上的操作,程序员和客户无需关心数据如何存储,只需要设计/使用操作即可。
2. Classifying Types and Operations
可变数据类型的对象:提供了可改变其内部数据的值的操作。
不可变数据类型:其操作不改变内部值,而是构造新的对象。
抽象数据类型包含的操作:构造器(creator)创建此数据类型的新对象,生产器(producer)根据旧的此数据类型产生新此数据类型,观察器(observer)通过某些值去返回这个数据类型的别的值,变值器(mutator)改变对象的属性。

构造器:既可以实现为构造函数,也可以实现为静态函数(工厂方法)。
变值器:通常返回void,返回void时说明其改变了对象的某些内部状态。
3. Abstract Data Type Examples
Int,String:不可变数据类型,没有变值器。
List:可变数据类型,有构造器addAll等。
4. Designing an Abstract Type
靠经验法则,提供一组操作,设计其行为规约spec。
(1)设计简洁,一致的操作。
(2)足够支持client对数据所做的所有操作,且用操作满足client的需要的难度要低。
(3)要么针对抽象设计,要么针对具体设计,不要混合设计。
5. Representation Independece(表示独立性)
表示独立性:client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应影响外部spec和客户端;除非ADT的操作指明了具体的前置条件和后置条件,否则不能改变ADT的内部表示。
6. Testing an Abstract Data Type
测试creators, producers, and mutators:调用observers来观察这些 operations的结果是否满足spec;
测试observers:调用creators, producers, and mutators等方法产生或改变对象,来看结果是否正确。
风险:如果被依赖的其他方法有错误,可能导致被测试方法的测试结果失效。
7. Invariants(不变量)
好的ADT可以负责其不变量,与client的任何行为无关;当一个ADT能够确保它内部的不变量恒定不变(不受使用者/外部影响),我们说这个ADT保护自己的不变量。
需要不变量的原因:保持程序的正确性,容易发现错误。比如分析时可以依赖字符串不变性来跳过关于字符串的代码;基于字符串建立其他的不变量时也更容易。
对变量使用private和final使成员变量无法改变。
如果ADT泄露一个mutable对象的引用,会违背immutable类型的约定。
解决方法:防御性复制:假定用户毁坏不变性,确保类不变量在任何输入下都有效。通常,应该检查所有ADT操作的参数类型和返回类型;如果任何类型都是可变的,请确保实现不会返回对其形式的直接引用。
为避免表示泄露,最好的方法是使用immutable类型,彻底避免表示泄露。
8. Rep Invariant and Abstraction Function
R:由实际实施实体的值组成。一般情况下ADT表示比较简单,有时需要复杂表示。
A:抽象值构成的空间:client看到和使用的值。它们不存在但形式上是抽象类型元素。
ADT开发者关注空间R,client关注抽象空间A。
每个A要有至少一个R的元素对应(满射但未必单射),R元素可以不对应A。
AF:抽象函数:R和A之间映射关系的函数,即如何去解释R中的每一个值为A中的每一个值,包括满射、非单射、未必双射(R中的部分值并非合法的,在A中无映射值)。
RI:表示不变性,某个具体的表示是否是合法的,如果合法的话就对应true。因此,RI可以看作所有表示值的一个子集,包含了所有合法的表示值。
不同的内部表示,需要设计不同的AF和RI;选择某种特定的表示方式R,进而指定某个子集是合法的RI,并为该子集中的每个值做出解释AF(如何映射到抽象空间的值)。
设计ADT:(1)选择R和A;(2)RI :合法的表示值;(3)如何解释合法的表示值:映射AF;做出具体的解释:每个rep value如何映射到abstract value。
在所有可能改变rep的方法内部都要检查(用checkRep函数)。
9. Beneficent mutation
抽象值永远不应该改变,但是只要rep值继续映射到相同的抽象值,实现就可以自由的修改rep值,这样客户机就看不到更改,被叫做有益的可变性。
对immutable的ADT来说,它在A空间的抽象值应是不变的,但其内部表示的R空间的取值是可以变化的。
作用:(1)例如通过cache暂存某些频繁计算的结果.;
(2)例如对tree数据结构进行插入或删除节点之后;
(3)在toString()的时候才进行约分计算。
通过牺牲部分immutability的原则来换取效率和性能。
10. Documenting the AF, RI, and Safety from Rep Exposure
精确记录RI:解释字段如何有效;精确记录AF:如何解释每一个R值。
表示泄露的安全声明:给出理由证明代码未对外泄露内部表示。
ADT的规约里只能用client可见的内容来写,包括参数,返回值,异常等;如果规约里提到值,只能用A空间的值。
构造器和生产器在创建对象时要确保不变量不变;变值器和观察器在执行时必须保持不变性;在每个方法return之前用checkRep函数检查不变量。

11. ADT invariants replace preconditions
用ADT不变量取代复杂的前置条件,相当于将前置条件封装到ADT内部。
四.面向对象的编程(OOP)(ADT的具体实现)
1. Basic concepts: object, class, attribute, and method
对象:包括状态(对象的数据)和行为(对象支持的动作)。
类:对象的实现,包括数据(field)和方法(method)。
类成员变量和类方法(与类相关)/实例成员变量和实例方法(与类无关)。
静态方法和实例方法的区别:

2. Interface and Enumerations
接口中只有方法的定义,没有实现;接口之间可以继承与扩展;一个类可以实现多个接口(从而具备多个接口中的方法);一个接口可以有多种实现类。
接口确定ADT规约,类实现ADT;也可以直接使用类作为ADT,有ADT定义和实现。
如果接口定义中没有cconstructor,client就需要直到该接口中某个具体实现类的名字。解决方法:在接口中使用静态方法代替构造器,返回新建的具体实现。
接口中的每个方法在所有类中都要实现,导致部分方法被大量重复实现。我们通过default方法,在接口中统一实现某些功能,无需在各个类中重复实现它。
3. Encapsulation and information hiding
信息隐藏/封装:对其他模块隐藏内部数据和其他所有实现细节。
接口中的信息隐藏:使用接口类型声明变量;客户端仅使用接口中定义的方法;客户端代码无法直接访问属性。

4. Inheritance and Overriding
(1)重写:子类对超类中的方法进行重新实现。
严格继承(strict inheritance):子类只能添加新方法,无法重写超类中的方法(final)。
重写之后,可以利用super()复用父类中函数的功能,并对其进行扩展。
用super复用父类的构造器时,应该把super放在第一句。
重写时签名要保持一致(用@Override签名),编译器会检查是否一致。
(2)抽象类:含有至少一个抽象方法的类。
抽象方法:只有定义没有实现,由abstract关键字定义的方法。
抽象类不能实例化(new对象);继承某个抽象类的子类在实例化时,所有父类的抽象方法必须已经实现。
接口是只有抽象方法的抽象类。
5. Polymorphism, subtyping and overloading(多态,子类型,重载)
(1)Three Types of Polymorphism:
Ad hoc polymorphism(特殊多态):一个方法可以有多个同名的实现(方法重载);
Parametric polymorphism(参数化多态):一个类型名字可以代表多个类型(泛型编程);
Subtyping(also called subtype polymorphism or inclusion polymorphism 子类型多态、包含多态):一个变量名字可以代表多个类的实例(子类型)。
(2)Ad hoc polymorphism and Overloading:
重载:多个方法具有同样的名字,但有不同的参数列表或返回值类型。
价值:方便client调用,可以用不容的参数列表调用同样的函数。
重载是一种静态多态,根据参数列表进行最佳匹配,在编译阶段决定具体执行哪个方法;而重写是在运行时进行动态检查。
(3)Parametric polymorphism and Generic programming
参数多态性是指方法针对多种类型时具有同样的行为(这里的多种类型应具有通用结构),此时可使用统一的类型变量表达多种类型。在运行时根据具 体指定类型确定具体类型(编译成class文件时,会用指定类型替换类型变量“擦除”)。
泛型编程是一种编程风格,其中数据类型和函数是根据待指定的类型编写的,随后在需要时根据参数提供的特定类型进行实例化。
泛型编程围绕“从具体进行抽象”的思想,将采用不同数据表示的算法进行抽象,得到泛型化的算法,可以得到复用性、通用性更强的软件。
使用泛型变量的三种形式:泛型类、泛型接口和泛型方法。
类中如果声明了一个或多个泛型变量,则为泛型类,这些类型变量称为类的类型参数;接口中中如果声明了一个或多个泛型变量,则为泛型接口。
(4)Subtyping Polymorphism
一个类只有一个父类,但可以实现多个接口。
子类型的规约不能弱于超类的规约。
子类型多态:不同类型的对象可以统一处理,无需区分,从而隔离了变化。
Liskov Substitution Principle (LSP):如果S是T的子类,那T的对象可以替换为S的对象(任意子类型的对象)。
6. Some important Object methods in Java
比较两个对象相等的函数equals和hashCode(求哈希值)配合使用进行对象的比较。在新建两个同类型的大小相同的副本时,他们的哈希值由于hashCode重写,导致哈希值不同,比较结果会输出不相等。解决方法如下:

7. Designing good classes

五.ADT和OOP之间的等价性
1. Equivalence Relation(等价关系)
基于抽象函数AF定义ADT的等价操作。现实中每个对象实体都是独特的,所以无法完全相等,但会有“相似性”。数学上的等价关系:自反,对称,传递。
2. Equality of Immutable Type(不可变类型的相等性)
将数据类型的具体实例映射到相应的抽象值,AF映射到同样的结果,则等价。
从外部观察者角度,对两个对象调用任何相同的操作,都会得到相同的结果,则认为这两个关系是等价的。
3. == vs equals
==具有引用等价性;equals操作比较对象内容,是对象等价性。
如果用==,是在判断两个对象身份标识ID是否相等(指向内存里的同一段空间)。
对每个抽象数据类型,都需要用Override重写一遍equals。
4. Implementing equals
在Object中实现的equals是判断引用等价性,通常不是需要的,所以需要重写。

instanceof操作符测试对象是否是特定类型的实例。
instanceof只能在equal方法上用,其他地方禁用,禁用的也包括检查对象运行时类型的其他方法。
5. The Object contract(对象契约)
重写equals的约定:等价关系:自反,传递,对称;除非对象被修改了,否则调用多次equals应该得到同样的结果;相等的对象,其hashCode的结果应当一致。
哈希表是映射的一种表示:一种将键映射到值的抽象数据类型。
当一个键和一个值要插入时,我们计算键的哈希代码,并将其转化为数组范围内的索引,然后在该索引处插入这个值。哈希表的rep不变量包含一个基本约束,即键位于由其哈希代码确定的插槽中。
在应用程序执行期间,每当在同一对象上多次调用hashCode方法时,只要该对象上的equals比较中使用的信息未被修改,hashCode方法必须始终返回相同的整数。
从应用程序的一次执行到同一应用程序的另一次执行,这个整数不需要保持一致。
如果根据equals(Object)方法,两个对象是不等的,那么对这两个对象中的每一个调用hashCode方法都必须产生不同的整数结果,这一点是不需要的(但性能会变差)。
Object的default的hashCode返回对象的内存地址。
重写hashCode的标准:为用于确定相等性的对象的每个组件计算一个哈希代码(通常通过调用每个组件的hashCode方法),然后将它们结合起来,加入一些算术运算。
6. Equality of Mutable Types
观察等价性:在不改变状态的条件下,两个mutable对象是否看起来一致。
行为等价性:调用对象的任何方法都展示出一致的结果。
Java对大多数可变数据类型(比如集合)使用严格的观察等价性,而对其他可变类(比如StringBuffer)使用行为等式。
如果将可变对象用作集合元素,必须非常小心。如果某个mutable的对象包含在Set集合类中,当其发生改变后,集合类的行为不确定。
对可变类型,实现行为等价性即可;即只有指向同样内存空间的objects才是相等的。因此对可变类型来说无需重写equals和hashCode,,直接继承即可。如果一定要判断两个可变对象看起来是否一致,最好定义新的方法。
7.类型自动转换与等价性
Int可以直接==判断,但Integer只能用equals的对象方法判断。
但-128——127的Integer是可以用==判断的。

浙公网安备 33010602011771号