软件构造知识点理解与总结:
目录
- 软件构造:开发高质量的软件系统
- 软件构造的多维度视图和质量目标
- 软件测试与测试优先的编程
- 软件构造过程与配置管理
- 数据类型与类型检验
- 设计规约
- 抽象数据类型(ADT)
- 面向对象的编程(OOP)
- ADT和OOP中的“等价性”
- 面向复用的软件构造技术
- 面向可维护性的构造技术
- 面向可复用性和可维护性的设计模式
- 面向正确性与健壮性的软件构造
0.软件构造:开发高质量的软件系统
设计目标:编程的“视野” 设计原则:编程的“标尺” 设计模式:编程的“经验”
1.软件构造的多维度视图和质量目标
1.软件构造的多维度视图
按阶段划分:构造时/运行时视图 按动态性划分:时刻/阶段(形态)视图 按构造对象的层次划分:代码(逻辑)/构件(物理)视图
2.阶段划分、各阶段活动
3.软件质量指标
(1)外部指标(影响用户) ①正确性 按照预先定义的“规约”执行,至高无上的质量指标。 需要在软件系统的每一层保证自己的正确性,同时假设其下层是正确的。 通过测试和调试、防御式编程、形式化方法等手段保证正确性 ②健壮性 针对异常情况(取决于spec的范畴)的处理,出现异常时不要“崩溃”。 出现规约定义之外的情形的时候,软件要做出恰当的反应,是对正确性的补充。 未被specification覆盖的情况即为“异常情况” ③可扩展性 对软件的规约进行修改,是否足够容易? 两种原则:简约主义设计,分离主义设计 ④可复用性 发现共性,一次开发,多 次使用 不要不断重复自己写过的东西,不要重复造轮子 ⑤兼容性 不同的软件系统之间相互可容易的集成 需要保证设计的同构性: -标准统一的文件格式 -标准统一的数据结构 -标准统一的用户接口 -标准统一的访问协议 ⑥性能 性能是软件系统对硬件资源尽可能少地提出要求的能力,例如处理器时间、内部和外部存储器占用的空间、通信设备中使用的带宽。 性能毫无意义,除非有足够的正确性 对性能的关注要与其他质量属性进行折中 过度的优化导致软件不再适应变化和复用 ⑦可移植性 软件可方便的在不同的技术环境(硬件、操作系统)之间移植’ ⑧易用性 容易学、安装、操作、监控 给用户提供详细的指南 结构简单,了解用户。 ⑨功能性 功能性是系统提供的可能性的程度。 程序设计中一种不适宜的趋 势,即软件开发者增加越来越多的功能,企图跟上竞争,其结果是程 序极为复杂、不灵活、占用过多的磁盘空间 每增加一小点功能,都确保其他质量属性 不受到损失 ⑩及时性 软件系统在用户需要时或之前发布的能力。 (2)内部指标(影响软件本身和它的开发者,外部质量取决于内部质量) 源代码相关因素,如代码行数(loc)、循环复杂性等 体系结构相关因素,如耦合、内聚等 可读性 可理解性 清晰度 大小 (3)指标折中 正确的软件开发过程中,开发者应该将不同质量因素之间如何做出折中的设计决策和标准明确的写下来 虽然需要折中,但“正确性”绝不能与其他质量因素折中。 最重要的几个质量因素:正确性、健壮性(可靠),复用性、可扩展性(模块化)
2.软件测试与测试优先的编程
§ 认可“测试”的价值,搞清楚“测试优先”的哲理 § 学会用等价划分和边界值分析方法为模块设计测试用例 § 可用工具度量一组测试用例对代码的“覆盖度” § 各种各样的测试,都有些初步了解 大纲: (1)软件测试 (2)测试用例 (3)测试优先的编程/测试驱动开发(TDD) (4)单元测试 (5)使用JUnit进行自动化单元测试 (6)黑盒测试 ——等价类划分 ——边界值分析 (7)白盒测试 (8)覆盖度 (9)集成测试 (10)回归测试 (11)在程序中文档化测试策略
黑盒测试用例的设计
– 等价类划分、边界值分析
以注释的形式撰写测试策略
JUnit测试用例写法
测试覆盖度
什么是测试? (1)软件测试是提高软件质量的重要手段 发现bugs, 确认是否达到可用级别(用户需求) 关注系统的某一侧面的质量特性 (2)即使是最好的测试,也无法达到100%的无错误 (3)每 kloc(1000 行代码)的一些典型残留缺陷率(软件发布后遗留的错误): ——1-10 个缺陷/kloc:典型的行业软件。 ——0.1-1 缺陷/kloc:高质量验证。 Java 库可能达到这种正确性水平。 ——0.01-0.1 缺陷/kloc:最好的安全关键验证。NASA 和 Praxis 这样的公司可以达到这个水平。 (4)这对于大型系统来说可能令人沮丧。 例如,如果你已经发布了 100 万行典型的行业源代码(1缺陷/kloc),这意味着您错过了 1000 个错误! (5)测试跟其他活动的目标相反:破坏、证错、“负能量” (6)再好的测试也无法证明系统里不存在错误 (7)什么是好的测试: ——能发现错误 ——不冗余 ——最佳特性,多种测试方案中最好的 ——别太复杂也别太简单
测试:发现是否存在错误 调试:识别错误根源,消除错误
==白盒测试:==对程序内部代码结构的测试 ==黑盒测试:==对程序外部表现出来的行为的测试 什么是测试用例: test case = {test inputs + execution conditions+ expected results} 测试用例:输入+执行条件+期望结果
好的测试用例的特点: (1)最可能发现错误 (2)不重复、不冗余 (3)最有效 (4)既不简单也不复杂
测试优先的编程: 定义:在编写代码之前先编写测试。 为什么要进行测试优先的编程: (1)尽早并经常进行测试,使程序快速出错。 (2)不要把测试留到最后,当有一堆未验证的代码,将测试留到最后只会使调试时间更长,因为错误可能在代码的任何地方。 (3)“测试代码”比写代码更有成就感 过程: (1)先写spec (2)再写符合spec的测试用例 (3)写代码、执行测试、有问题再改、再执行测试用例,直到通过
Unit Testing 单元测试: 定义:针对软件的最小单元模型开展测试,隔离各个模块,容易定位错误和调试 黑盒测试: 定义:用于检查代码的功能,不关心内部实现细节
等价类划分 边界值 白盒测试 代码覆盖度 回归测试
3.软件构造过程与配置管理
了解软件构建的一般流程(广义的软件构造过程): 设计—>编程—>调试—>测试—>构建—>发布
大纲: ? 软件开发生命周期(SDLC) ? 传统软件过程模型(瀑布、增量、V字、原型、螺旋) ? 敏捷开发和极限编程(XP) ? 软件配置管理(SCM) ? Git作为SCM工具 ? 总结
软件开发生命周期(SDLC): From 0 to 1 从无到有
软件生命周期中的多个版本: From 1 to n 从有到好
传统软件过程模型: Linear 线性过程 Iterative 迭代过程 敏捷开发和极限编程(XP): 敏捷开发:通过快速迭代和小规模的持续改进,以快速适应变化。
软件配置管理(SCM): 定义:追踪和控制软件的变化 核心:版本控制和基线的确立 软件配置项:软件中发生变化的基本单元(例如:文件) 基线:软件持续变化过程中的“稳定时刻”(例如:对外发布的版本) 版本:为软件的任一特定时刻(Moment)的形态指派一个唯一的编号,作为“身份标识”
Git作为SCM工具 Refactoring重构:在不改变功能的前提下优化代码
4.数据类型与类型检验
大纲:
- 编程语言中的数据类型
- 静态与动态数据类型检查
- 可变性和不变性
- 快照图
- 复杂数据类型:数组和集合
- 有用的不可变类型
- 总结
数据类型:一组值以及可以对其执行的操作 变量:用特定数据类型定义,可存储满足类型约束的值
5.设计规约
大纲:
- 编程语言中的函数/方法
- 规范:通讯编程
为什么需要规范 行为对等 规范结构:前置条件和后置条件 测试和验证规格 - 设计规范
分类规格 制图规格 规范的质量 - 总结
写在方法前面的说明性注释,用来解释方法的功能、参数、返回值等。如下图所示: 规约的好处 记录自己的设计决策,供自已或他人阅读。 团队协作中,如果没有规约就无法分派任务,无法写程序;即使写出来,也无法验证对错。 规约作为一种客户端与程序之间的“契约”,可以明确双方的责任。 规约可以隔离变化,无需通知客户端,即不管具体实现如何修改,只要符合规约,客户端就没必要知道修改的具体内容。 达到解耦的目标。 可以利用规约来判定方法的行为等价性。 测试用例可以根据规约来编写。
(方法的)规范和合同 规格(或称为合同) ? 规范是团队合作的关键。没有规范就不可能委派实现方法的责任。 ? 规范充当一个契约,实现者负责满足契约,而使用该方法的客户机可以依赖该契约。 –说明方法和调用方的职责 –定义实现正确的含义 ? 规范对双方都有要求:当规范有前提时,客户也有责任。
为什么需要规格? ? 现实:程序中许多最糟糕的错误都是由于对两段代码之间的接口行为的误解而产生的。 –虽然每个程序员都有自己的规范,但并不是所有的程序员都把它们写下来。因此,团队中的不同程序员有不同的规范。 –当程序失败时,很难确定错误在哪里。 ? 优点:代码中的精确规范可以让您将责任分摊到代码片段上,并且可以让您免去为修复应该去哪里而费解的痛苦。 –规范对于方法的客户端来说很好,因为它们省去了读取代码的任务。
规范(合同) ? 该契约充当客户端和实现者之间的防火墙。 –它保护客户不受该单元工作细节的影响。 –它保护实现者不受单元使用细节的影响。 –此防火墙令客户端和程序不耦合,只要更改符合规范,允许独立更改单元代码和客户端代码。
行为等价性 为了确定行为等价性,问题是我们是否可以用一个实现代替另一个实现
规范结构:前置条件和后置条件 规格结构 ? 方法的规范由几个子句组成: –Precondition,由关键字requires指示 –Postcondition,由关键字effects指示 –Exceptional behavior:如果违反了Precondition,它会做什么 ? 先决条件是客户的义务(即方法的调用方)。它是调用方法的状态的条件。 ? 后置条件是方法实现者的义务。 ? 如果调用状态的前提条件成立,则该方法有义务通过返回适当的值、抛出指定的异常、修改或不修改对象等方式遵守后条件。
规格结构 ? 整体结构是一个逻辑含义:如果在调用方法时前置条件保满足了,则在方法完成时后置条件必须满足。? 如果在调用方法时前置条件不成立,则实现不受后置条件的约束。–它可以做任何事情,包括不终止、抛出异常、返回任意结果、进行任意修改等。
按照强弱分类 规约A强度>B规约强度意味着下面两点:
A的前置条件更弱 A的后置条件更强 前置条件和后置条件的强弱由条件的严格程度决定,即如果条件越放松,那么这个条件越弱。反之亦然。
如果要增强一个规约的强度,那么就意味着更松的前置条件+更严格的后置条件。
上图中第二个规约的前置条件比第一个规约的前置条件更加放松,因此规约变强。第三个规约的后置条件更加严格,所以强度更高。
但是要注意一点,后置条件的范围变大,并不意味着后置条件就变弱了,是否变弱需要看前置条件对应的那一部分后置条件是否变弱。如下图,规约二的前置条件变弱了,后置条件的范围变大,但是在满足规约一中的前置条件的情况下,此后置条件并没有变化,所以后置条件并没有变弱。因此整个规约二的强度变大了。
再看下面这个例子,在满足规约一的前置条件的情况下,规约二的后置条件相对于规约一的后置条件变弱了。但是由于前置条件也是变弱,因此无法比较这两个规约的强弱。 按照确定性分类 确定性规约(Deterministic):给定前置条件,其输出是唯一的、明确的。
欠定的规约(Underdetermined):同一个输入可以有多个输出,但是一旦使用具体的方法实现了这个规约,那么这个返回值也将会被确定。
非确定的规约(Not deterministic):同一个输入,多次执行可能得到多个结果,比如涉及到随机数的方法。
按照陈述性分类 操作式规约:有具体的实现细节,如伪代码
声明式规约:没有内部实现的描述,只有对输入输出的规定
声明式规约更有价值,更能应对变化。但是操作式规约能够方便开发。
6.抽象数据类型(ADT)
抽象数据类型与表示独立性: 能够分离程序中数据结构的形式和对其使用的方式
ADT的特性:不变量、表示泄漏、抽象函数AF、表示不变量RI 基于数学的形式对ADT的这些核心特征进行描述并应用于设计中。 大纲: 1.抽象和用户定义类型 2.ADT 中的操作分类 3.抽象数据类型示例 4.ADT的设计原则 5.代表独立性 (RI) 6.用Java实现ADT概念 7.测试ADT 8.不变量 9. Rep不变和抽象函数 10.良性突变 11.记录 AF、RI 和 Rep接触的安全性 12.ADT 不变量替换前提条件
AF: 满射、非单射、 未必双射 ? R中的部分值并非合法的,在A中无映射值
? 表示不变性RI:某个具体的“表示”是否是“合法的” ? 也可将RI看作:所有表示值的一个子集,包含了所有合法的表示值 ? 也可将RI看作:一个条件,描述了什么是“合法”的表示值
ADT操作的四种类型:
1.Creators 构造器: 用于创建一个新的对象,主要表现为构造函数。典型的有Integer.valueOf()。
2.Producers生产器: 通过接受同类型的对象以创建新的对象。典型的有String.contact()。(连接两个字符串,产生一个新的字符串)
3.Observers观察器 获取ADT内部的某个信息,而不改变ADT的现有状态。典型的有List.size()。(返回int,不同于原类型)
4.Mutators变值器(改变对象属性的方法) 改变对象内部的信息。典型的有List.add()。 通常为void,如果为void,则必然意味着它改变了某些对象的内部状态,也有可能返回非空类型(如容器类的put、add方法)
以上四类方法接受的参数类型和产生的返回值类型关系如下: 其中,T表示ADT本身的类型,t表示除了ADT之外的其他类型。“ * ” 表示出现0次或多次,“ + ” 表示出现1次或多次,“ | ” 表示或。
不变量(Invariants):
不变量是指在ADT创建之后就不会发生改变的量,在ADT存在的时间范围内恒为True,不受外部使用者的操作所影响。若ADT的不变量发生改变,则说明这个ADT已经发生变异,程序运行过程中的某个地方存在bug。
表示不变量(Representation Invariant)和抽象函数(Abstract Function):
表示空间R和抽象空间A 在介绍RI和AF之前,我们首先引入表示空间R和抽象空间A的概念。如下图: 表示空间R指的是开发人员实际实现时内部的值,而抽象空间A表示的是用户看到的和使用的值。开发人员更关注R,而用户更关注A。 表示值构成的空间(R/表示空间):实现者看到和使用的值。 抽象值构成的空间(A/抽象空间):client看到和使用的值
抽象函数AF: ??有了以上的两个概念,我们就可以给出抽象函数的概念。抽象函数即是描述从R到A的映射关系的函数,即如何将表示空间中的一个值解释为抽象空间中的一个值。AF通常是满足以下关系的一个映射:
AF是一个满射,即用户所看到或使用的任意一个值都是由一个表示值映射而来的。 AF未必是一个单射,即用户户所看到或使用的任意一个值可能由不止一个表示值映射而来。 AF未必是一个双射,即开发人员所面对的表示值中,会存在不满足前置条件的表示值,对于这类表示值不存在对于的抽象值。
表示不变量RI: ??由上面的讲述,我们知道了存在一定的表示值,其不满足规约中的前置条件,故无法进行映射。这就引出了一个问题:怎样的表示值无法被映射呢? 表示不变量RI就定义了这个问题,RI是由表示值空间R到boolean值的一个映射,其中映射为true的值可以被映射到抽象空间中,否则无法被映射。 ??我们可以这样理解,RI告诉了我们表示空间R中的值是否能被映射到抽象空间A中,也可以认为RI构成了表示空间R的一个子集,该子集中的值能够被映射到抽象空间A中。
以上概念的联系: ??在构建ADT时,我们需要有以下步骤: ??1. 选定表示空间(R) ??2. 进而找出其中满足条件的子集(RI) ??3. 并为子集中的每个元素做出对应的解释(AF) ??4. 最终将其映射到抽象空间(A)中。
表示独立性(Representation Independence) (RI):
ADT的表示独立性,要求客户在使用ADT时无需考虑ADT的内部实现,只需要根据ADT的规约进行使用即可。即ADT的内部实现变化不应该影响用户在客户端的使用。表示独立性的关键在于将数据结构的使用和数据结构自身的形式分离。防止因为用户在使用过程中假设ADT内部的实现,在假设的基础上形成依赖。
我们编写ADT追求的是表示独立性,避免表示泄露。
表示泄露(Representation Exposure):
表示泄露形容了一种类外部的代码可以直接修改类内部存储的数据的现象。通常是类外代码直接对类内的可变类型对象进行直接修改,导致了之前所提到过的引用别名现象,这种引用方式会导致我们对同一块内存进行多次引用,导致程序运行过程中的不确定性,恶意代码也可以十分简单的对ADT进行攻击,严重影响ADT的表示独立性和不变量。 ??为了防止表示泄露,我们通过可以采用以下几种方法:
- 使用private和final关键词对域进行修饰。
- 使用防御性拷贝,需要注意的是,防御性拷贝可以发生在传入和传出数据时。
- 通过规约对用户的行为进行限制。
- 使用不可变类型的数据构建ADT。
什么是表示泄露?先看个例子:
public class mySet {
private final Set<String> set = new HashSet<>();
public mySet() {}
public Set<String> getSet() {
return set;
}
public boolean addToSet(String add) {
return set.add(add);
}
我们提供了公有方法来给用于往集合里添加元素,然而用户完全可以用下面的方法修改这个集合
mySet s = new myset();
s.getSet().add("Whatever I wanna add");
甚至还能通过remove等一些方法来删除set中的元素,这些修改都能直接反映到我们的mySet类的实例s的set成员中,即mySet类中的表示set被泄露了,用户可以用mySet类以外的方法对mySet实例的set域进行修改。显然,只要我们返回了一个Set的对象,用户必然可以用java已经实现的方法来对这个对象进行修改,那么我们的问题就转移到怎样让用户的修改无法反映到mySet类的实例中的set域里。为了实现这个目的,我们可以返回set域的拷贝,而不是set域本身,这种方法被称为防御式拷贝:
public Set<String> getSet() {
return new HashSet<>(set);
}
这样用户如果用java自身实现的方法就不能对set域修改了,只能用ADT里实现的修改方法进行修改,这样就防止了表示泄露
以注释的形式撰写AF,RI:
不同的内部表示,需要设计不同的AF和RI
AF和RI都应该记录在代码中,紧挨着rep的声明。首先选择某种特定的表示方式R,进而指定某个子集是“合法”的(RI),并为该子集中的每个值做出“解释”(AF)——即如何映射到抽象空间中的值。
同样的表示空间R,可以有不同的RI 即使是相同的R、同样的RI,也可以有不同的AF,即“解释不同”
设计ADT:
- 选择R和A;
- RI——合法的表示值
- 如何解释合法的表示值——映射AF
做出具体的解释:每个rep value如何映射到abstract value。而且把这种选择和解释明确写到代码当中。
随时检查RI是否满足:checkRep()
有益的可变性:对immutable的ADT来说,它在A空间的abstract value应是不变的,但其内部表示的R空间中的取值则可以是变化的。
==在代码中用注释形式记录AF和RI:==不能在Javadoc文档中写,防止被外部看到而破坏表示独立性/信息隐藏
- 要精确的记录RI:rep中的所有fields何为有效
- 要精确的记录AF:如何解释每一个R值
- 表示泄露的声明:给出理由,证明代码并未对外泄露其内部表示
格式如下图所示:
7.面向对象的编程(OOP)
大纲: ? OOP的基本概念:对象、类、属性、方法、接口和枚举 ? OOP 的独特特征 ——封装与信息隐藏 ——继承与重写 ——多态、子类型、重载 ——静态与动态分派 ? Java 中一些重要的 Object 方法 ? 设计好的课程 ? OOP的历史 ? 总结
接口,抽象类,具体类
类:实现抽象数据类型ADT。 接口:确定ADT的规约。接口之间可以继承与扩展;一个类可以实现多个接口;一个接口可以有多种实现类。
Interface和Class:定义和实现ADT 也可以不需要接口直接使用类作为ADT,既有ADT定义也有ADT实现。(更偏向使用接口来定义变量)
抽象方法:只有定义没有实现。具有方法名字但是没有具体实现的方法,需要有关键字abstract
抽象类:具有抽象方法的类;抽象类不能实例化。(不能用new生成对象)一个类至少包含一个抽象方法。 抽象接口:只含有抽象方法的抽象类 具体类:其中的所有方法都必须实现!!
继承,override
严格继承:子类只能添加新方法,无法重写超类中的方法。若要防止重写超类中的方法,超类中的方法需要用关键字final修饰。
public class Car{
public final void drive(){...} //不能在子类重写
}
override:重写的函数:完全同样的signature(形参,函数名,返回类型);实际执行时调用哪个方法,运行时决定,在重写的方法前,需要使用@Override关键字。若想在重写的函数中使用超类中的方法,使用super关键字。
ps:
- 父类型中被重写函数体不能为空:意味着对其大多数子类型来说,该方法是可以被直接复用的;对某些子类型来说,有特殊性,故重写父类型的函数,实现自己的特殊要求。
- 如果父类型中的某个函数实现体为空,意味着其所有子类型都需要这个功能,但各有差异,没有共性,在每个子类中均需要重写。
- overriden methods是在run-time进行动态类型检查。
- 重写之后,利用super()复用父类型中函数的功能,并对其进行扩展。eg.super.message(); 重写的时候,不要改变原方法的本意。
- 继承某个抽象类的子类在实例化时,所有父类中的抽象方法必须已经实现
- 如果某些操作时所有子类型都共有,但彼此有差别,可以在父类型中设计抽象方法,在各子类型中重写。
- 所有子类型完全相同的操作,放在父类型中实现,子类型中无需重写。
- 有些子类型有而其他子类型无的操作,不要在父类型中定义和实现,而应在特定子类型中实现。
多态,overload
多态(polymorphism):
- 特殊(Ad hoc)多态:一个方法可以有多个同名的实现(方法/功能重载);
- 参数化(Parametric)多态:一个类型名字可以代表多个类型(泛型编程);
- 子类型多态、包含(Subtyping)多态:一个变量名字可以代表多个类的实例(子类型);
重载overload(特殊多态):多个方法具有同样的名字,但有不同的参数列表或返回值类型。 overload的价值:方便client调用,client可以用不同的参数列表,调用同样的函数。 Overloading is a static polymorphism(重载是静态多态):根据参数列表进行最佳匹配;静态类型检查;在编译阶段时决定要具体执行哪个方法。
overloading rules:
- 不同的参数列表
- 相同/不同的返回值类型
- 相同/不同的public/private/protected
- 可以定义相同/不同的异常
- 可以子啊同一个类内重载,也可以在子类中重载
参数多态性是指方法针对多种类型(具有通用的结构)时具有相同的行为,此时可使用统一的类型表达多种类型;在运行时根据具体指定类型确定(编译成class文件时,会用指定类型替换类型变量)
泛型
泛型编程是一种编程风格,其中数据类型和函数是根据待定的类型编写的,随后在需要时根据参数提供的特定类型进行实例化。
ps:类中如果声明了一个或多个泛型变量,则为泛型类;泛型接口(字面意思)
通配符:只在使用泛型的时候出现,不能在定义中出现。 eg.List<?> list=new ArrayList(); List<? extedns Animal> List<? super Animal>
子类型多态:不同类型的对象可以统一的处理而无需区分,从而隔离了“变化”
8.ADT和OOP中的“等价性”
大纲: ? 等价关系 ? 看待平等的三种方式 ? == 与 equals() ? 实现equals() ? 对象合同 ? 可变类型的相等性 ? 自动装箱和平等
等价性equals()和==
基于抽象函数AF定义ADT的等价操作,如果AF映射到同样的结果,则等价
==:引用(地址)等价性;对基本数据类型,使用等号判定相等 equals():对象等价性;对对象类型,使用equals
ps:如果对象数据类型使用等号判断是否相等,是在判断两个身份对象身份表示ID是否相等(指向内存里的同一段空间)
instanceof:进行类型比较和null值判定;判断某个对象是不是特定类型(或其子类型);动态类型检查
严格来说,在没有AF的情况下直接在equals()中判断每个域的等价性,是不正确的;
equals的自反,传递,对称
用“是否为等价关系”检验重写的equals()是否正确 自反:对任意非空的引用变量x,x.equals(x)必须返回true 对称: 对任意非空的引用变量x、y,若x.equals(y)返回true,则y.equals(x)返回true 传递:对任意非空的引用变量x、y、z,若x.equals(y)返回true且y.equals(z)返回true,则x.equals(z)也返回true
此外,对于任意非空的引用变量x,则x.equals(null)返回false
hashCode()
程序中多次调用同一个对象的hashCode方法,都要返回相同的值
若两个对象相等,则它们的hashCode相等;反之,若两个对象不相等,则它们的hashCode不相等
当重写Object中的equals()时,也要重写hashCode(),例:
不变对象的引用等价性,对象等价性
不可变类型必须重写equals()和hashCode()。
可变对象的观察等价性,行为等价性
注意:如果某个mutable对象包含Set集合类中,当其发生改变后,集合类的行为不确定。
对可变类型,实现行为等价即可(equals()should implement behavioral equality)。就是说,只有指向同样内存空间的objects,才是相等的。
对可变类型来说,无需重写equals()和hashCode(),直接继承Object的两个方法即可
9.面向复用的软件构造技术
大纲: ? 什么是软件重用? ? 如何衡量“可重用性”? ? 可重用组件的级别和形态 ——源代码级别的复用 ——模块级别的复用:类/抽象类/接口 ——库级别的复用:API/包 ——系统级别的复用:框架 ? 设计可复用的类 ——继承与重写 ——重载 ——参数多态与泛型编程 ——行为子类型和 Liskov 替换原则 (LSP) ——组合与委托 ? 设计可复用库与框架 ——API 和库 ——框架 ——Java 集合框架(一个例子)
Programing for/with reuse
programming for reuse(面向复用编程):开发出可复用的软件 programming with reuse(基于复用编程):利用已有的可复用软件搭建应用系统
优点:
- 很大的适应性
- 降低成本和开发时间
- 充分的测试→高可靠、稳定
- 标准化、在不同应用中保持一致
缺点:针对性不强→性能差(代价高)
白盒复用:源代码可见,可修改和扩展。(复制已有代码到正在开发的系统,进行修改) 优点:可定制化程度高 缺点:对其修改增加了软件的复杂度,且需要对其内部充分的了解 黑盒复用:源代码不可见,不能修改。(只能通过API接口来使用,无法修改代码) 优点:简单,清晰 缺点:适应性差些
LSP
- 子类必须完全的实现父类的方法(不能删父类的方法;子类型需要实现抽象类型中的所有未实现的方法)
- 子类可以有自己的个性(子类型可以增加方法)
- 子类型中重写的方法必须使用同样类型的参数或者符合co-variance的参数(此种情况Java目前按照overload处理)
- 子类型中重写的方法不能抛出额外的异常(协变)
- 覆盖和实现父类的方法时输入参数可以被放大(更弱的前置条件/逆变)
- 覆盖和实现父类的方法时输出参数可以被缩小(更强的后置条件/协变)
- 更强/保持的不变量
协变、反协变
数组的子类型化
泛型的子类型化
泛型中的通配符(?)
可采用通配符实现两个泛型中的协变
只有在下面这种情况下可以用无限定通配符 ? 当实现某方法的代码不依赖于类型参数(不调用其中的方法),如List中的方法;或者只依赖于Object类中的功能,如toString(),HashCode(),Clone()…
下限通配符 <? super A>:eg.List<? super Integer> 上限通配符<? extends A>:eg.List<? exteds Number>
Delegation
Comparator和Comparable
CRP原则
接口的组合
白盒框架的原理与实现
黑盒框架的原理与实现
10.面向可维护性的构造技术
大纲: ? 软件维护和演进 ? 可维护性指标 ? 模块化设计和模块化原则 ? 面向对象设计原则:SOLID ? 基于语法的构建 ——语法和解析器 ——正则表达式(regexp)
可维护性的常见度量指标
聚合度与耦合度
SOLID
语法、正则表达式
11.面向可复用性和可维护性的设计模式
大纲: ? 创作模式 ——工厂方法模式在不指定确切类的情况下创建对象。 ? 结构模式 ——适配器允许具有不兼容接口的类通过将自己的接口包装在已经存在的类的接口上。 ——装饰器动态添加/覆盖对象方法中的行为。 ? 行为模式 ——策略允许在运行时选择一系列算法中的一个。 ——模板方法将算法的骨架定义为抽象类,允许其子类提供具体的行为。 ——迭代器顺序访问对象的元素而不暴露它的底层表示。 ——访问者通过移动对象结构将算法与对象结构分开方法的层次结构到一个对象中。
设计模式adapter、decorator、strategy、template、iterator/iterable、factory method、visitor
12.面向正确性与健壮性的软件构造
大纲: ? 什么是稳健性和正确性? ? 如何衡量稳健性和正确性? ? Java 中的错误和异常 ? 异常处理 ? 断言 ? 防御性编程 ? SpotBugs 工具 ? 总结
健壮性和正确性
Throwable
Error/Runtime异常、其他异常
Checked异常、Unchecked异常
Checked异常的处理机制:
声明、抛出、捕获、处理、清理现场、释放资源等
自定义异常类
断言的作用、应用场合
防御式编程的基本思路
参考: link (2道大题) link link link link link (18年部分答案)
|