软件构造复习手册2

第三部分 ADT&OOP

基本数据类型:int double float short byte char boolean long 储存在栈区。不可变。

对象引用类型:class、interface、array,etc 储存在堆区。String不可变,其他的均可变。

  • 静态类型检查
    • 关于“类型的检查”,不考虑值
    • 在编译阶段发现错误,避免将错误带入运行阶段
    • 提高程序的正确性、健壮性
  • 静态类型检查错误:
    • 语法错误
    • 类名/函数名错误
    • 参数数目错误
    • 参数类型错误
    • 返回值类型错误
  • 动态类型检查
    • 关于“值”的检查
  • 动态类型检查错误:
    • 非法的参数值
    • 非法的返回值
    • 越界
    • 空指针

Mutable可变 & Immutable不可变

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

mutable 优点:

  • 拷贝:不可变类型,频繁修改会产生大量的临时拷贝,需要垃圾回收;可变类型,最少化拷贝,以提高效率
  • 获得更好的性能
  • 模块之间共享数据

UnmodifiableCollections:Java设计有不可变的集合类提供使用

值的改变 & 引用的改变

  • “改变一个变量”:将该变量指向另一个值的存储空间(引用)
  • “改变一个变量的值”:将该变量当前指向的值的存储空间中写入一个新的值

表示泄露和防御式拷贝

通过防御式拷贝,给客户端返回一个全新的对象(副本),客户端即使对数据做了更改,也不会影响到自己。例如:

return new Date(groundhogAnswer.getTime());

大部分时候该拷贝不会被客户端修改,可能造成大量的内存浪费
如果使用不可变类型,则节省了频繁复制的代价

Snapshot diagram

运行时、代码层面、瞬时

  • 基本类型的值
  • 对象类型的值
    • 可变对象:单线圈
    • 不可变对象:双线圈
    • 不可变的引用:双线箭头
    • 可变的引用:单线箭头
      • 引用是不可变的,但指向的值却可以是可变的
      • 可变的引用,也可指向不可变的值

3.2 设计规约(Specification)

Spec概念

程序和客户端达成的一致

作用:

  • 给“供需双方”都确定了责任并区分责任,调用时双方都要遵守(客户端只需要理解Spec即可)
  • 隔离“变化”、降低耦合度
  • 不需要了解具体实现

要素:

  • 输入数据类型(客户端约束)
  • 输出数据类型(内部实现约束)

前置条件 & 后置条件

  • 前置条件:For 客户端
  • 后置条件:For 开发者
  • 契约:
    前置条件满足了,后置条件必须满足;
    前置条件不满足,后置条件不一定满足(输入错误,可以抛出异常)。

行为等价性

  • 站在客户端角度、根据规约:功能是否等价

例:以下两段代码是否等价

规约:

解:

  • val在范围内时,两者返回相同;
  • val不在范围内时,前者返回arr.length,后者返回-1
  • 根据规约,两者效果相同,因此等价。

Spec的写法

  • 方法注释
  • @param
  • @return
  • @throws
  • 输入类型、返回类型

一个好的Spec应该:

  • 内聚的
    Spec描述的功能应单一、简单、易理解
    规约做了两件事,所以要分离开形成两个方法。
  • 信息丰富的
    不能让客户端产生理解歧义
  • 足够“强”
    太弱的spec,客户不放心
    开发者应尽可能考虑特殊情况,在post-condition给出处理措施
  • 足够“弱”
    太强的spec,在很多特殊情况下难以达到,给开发者增加了实现的难度(client当然非常高兴)

Spec的强度

  • 前置条件越弱,规约强度越强;
  • 后置条件越强,规约强度越强;
  • 规约越强,开发者责任越重,客户端责任越轻;

  • 某个具体实现,若满足规约,则落在其范围内;否则,在其之外。
  • 程序员可以在规约的范围内自由选择实现方式;
  • 更强的规约,表达为更小的区域;

3.3 抽象数据类型(ADT)

设计ADT:规格Spec–>表示Rep–>实现Impl

四类ADT操作

  • Creators
    • 实现:构造函数constructor或静态方法(也称factory method)
  • Producers
    • 需要有“旧对象”
    • return新对象
    • eg. String.concat()
  • Observers
    • eg. List.size()
  • Mutators
    • 改变对象属性
    • 若返回值为void,则必然改变了对象内部状态(必然是mutator)

表示独立性

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

抽象函数AF & 表示不变量RI

  • 抽象值构成的空间(抽象空间):客户端看到和使用的值
  • 程序内部用来表示抽象值的空间(表示空间):程序内部的值
  • Mapping:满射、未必单射(未必双射)

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

  • 抽象函数(AF)

    • R和A之间映射关系的函数
    • 即如何去解释R中的每一个值为A中的每一个值。
    • AF : R → A
    • R中的部分值并非合法的,在A中无映射值
  • 表示不变性(RI)

    • 某个具体的“表示”是否是“合法的”
    • 所有表示值的一个子集,包含了所有合法的表示值
    • 一个条件,描述了什么是“合法”的表示值
  • 检查RI:
    随时检查RI是否满足
    在所有可能改变rep的方法内都要检查
    Observer方法可以不用,但建议也要检查,以防止你的“万一”

测试ADT

因为测试相当于client使用ADT,所以它也不能直接访问ADT内部的数据域,所以只能调用其他方法去测试被测试的方法。

  • 针对creator:构造对象之后,用observer去观察是否正确

  • 针对observer:用其他三类方法构造对象,然后调用被测observer,判断观察结果是否正确

  • 针对producer:produce新对象之后,用observer判断结果是否正确

以注释的形式撰写AF、RI、 Safety from Rep Exposure

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

3.4 面向对象编程(OOP)

接口(Interface)& 抽象类(Abstract Class)& 具体类(Concrete Class)

接口:定义ADT
类:实现ADT

Concrete class --> Abstract Class --> Interface

接口

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

抽象类

  • 至少有一个抽象方法
  • 抽象方法 Abstract Method
    • 未被实现
    • 如果某些操作是所有子类型都共有,但彼此有差别,可以在父类型中设计抽象方法,在各子类型中重写

具体类

  • 实现所有父类未实现的方法

继承(Inheritance) & 重写(Override)

  • 类 & 类:继承
  • 类 & 接口:实现、扩展

覆盖/重写Override

  • 重写的函数:完全同样的signature
    • 实际执行时调用哪个方法,运行时决定
    • 重写的时候,不要改变原方法的本意
    • 运行阶段进行动态检查
    • 父类型中的被重写函数体
      • 不为空:
        • 该方法是可以被直接复用的
        • 对某些子类型来说,有特殊性,可重写父类型中的函数,实现自己的特殊要求
      • 为空:
        • 其所有子类型都需要这个功能
        • 但各有差异,没有共性,在每个子类中均需要重写

super

  • 重写之后,利用super()复用了父类型中函数的功能,还可以对其进行扩展
  • 如果是在构造方法中调用父类的构造方法,则必须在构造方法的第一行调用super()

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

多态(Polymorphism) & 重载(Overload)

重载:多个方法具有同样的名字,但有不同的参数列表或返回值类型。
Override和Overload

  • 特殊多态:功能重载

    • 方便client调用:client可用不同的参数列表,调用同样的函数
    • 根据参数列表进行最佳匹配
      • public void changeSize(int size, String name, float pattern) {}
      • 重载函数错误情况❌
        • public void changeSize(int length, String pattern, float size) {}:虽然参数名不同,但类型相同
        • public boolean changeSize(int size, String name, float pattern) {}:参数列表必须不同
    • 编译阶段时决定要具体执行哪个方法(与之相反,overridden methods则是在run-time进行dynamic checking)
    • 可以在同一个类内重载,也可在子类中重载
  • 参数化多态:使用泛型?编程

  • 子类型多态:期望不同类型的对象可以统一处理而无需区分,遵循LSP原则

3.5 ADT和OOP中的等价性

不可变对象的引用等价性 & 对象等价性

  • ==
    引用等价性
    相同内存地址
    对于:基本数据类型

  • equals()
    对象等价性
    对于:对象类型

  • 在自定义ADT时,需要用@Override重写Object.equals()(在Object中实现的缺省equals()是在判断引用等价性

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

equals() & hashCode()

equals()的性质:自反、传递、对称、一致性

  • equals()重写范例
    1. 判断引用等价性
    2. 判断类的一致性
    3. 判断具体值是否满足等价条件(自定义)
  • 以Lab3的Plane为例
    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Plane)) {
            return false;
        }
        Plane plane = (Plane) o;
        return Objects.equals(number, plane.number) && Objects.equals(strType, plane.strType)
                && intSeats == plane.intSeats && age == plane.age;
    }

instanceof

  • 判断类
  • 仅在equals里使用

hashCode()

  • 等价的对象必须有相同的hashCode
  • 不相等的对象,也可以映射为同样的hashCode但性能会变差
  • 自定义ADT要重写hashcode
  • 返回值是内存地址

可变对象的观察等价性 & 行为等价性

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值