SoftwareConstruction——ADT&OOP

一、Java数据类型

1 数据类型

基本数据类型 & 对象数据类型

PrimitivesObject Reference Types
int, long, byte, short, char, float, double, booleanClasses, interfaces, arrays, enums, annotations
只有值,没有ID (与其他值无法区分)既有ID,也有值
不可变可变/不可变
中分配内存中分配内存
Can’t achieve unity of expressionUnity of expression with generics
代价低代价昂贵

2 静态类型检查 & 动态类型检查

静态类型检查

  • 关于“类型的检查”,不考虑值
  • 在编译阶段发现错误,避免将错误带入运行阶段
  • 提高程序的正确性、健壮性

静态类型检查错误

  • 语法错误
  • 类名/函数名错误
  • 参数数目错误
  • 参数类型错误
  • 返回值类型错误

动态类型检查

  • 关于“值”的检查

动态类型检查错误

  • 非法的参数值
  • 非法的返回值
  • 越界
  • 空指针

3 Mutable可变 & Immutable不可变

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

不变性Immutability:一旦被创建,其值不能改变。如果是引用类型,也可以是不变的。一旦确定其指向的对象,不能再被改变,这时我们就会用final来限定,使得一个引用成为不可变类型(如果编译器无法确定final变量不会改变,就提示错误,这也是静态类型检查的一部分)

final

  • 尽量使用final作为方法的输入参数、作为局部变量
  • final表明了程序员的一种“设计决策”
  • final类无法派生子类
  • final变量无法改变值/引用
  • final方法无法被子类重写

不变对象:一旦被创建,始终指向同一个值/引用。比如String、LocalDateTime类型变量修改内容时需要指向一个新的内存。使用不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收) ,但不可变类型更“安全”, 在其他质量指标上表现更好。
可变对象:拥有方法可以修改自己的值/引用。比如StringBuilder、Date类型变量有方法可以对其内容修改而不改变指向。可变类型最少化拷贝以提高效率和性能,也适合于在多个模块之间共享数据。

/* String部分 */
String s = "a";    //开辟一个存储空间,里面存着字符a,s指向这块空间,记为space1
String t = s;    //让t指向s所指向的空间即space1
s = s.concat("b"); //把字符a和字符b连接,然后把“ab”放在一个新的存储空间,记为space2,最后让s指向这块空间
//我们可以看到,现在s和t所指向的是两块不同的空间,空间中的内容也不一样,因此s和t的效果是不一样的

/* StringBuilder部分 */
StringBuilder sb = new StringBuilder("a"); //开辟一个存储空间,里面存着字符a
StringBuilder tb = sb;    //开辟一个存储空间,里面存着字符a
sb.append("b");    //取出a,然后与字符b连接,然后把“ab”仍然放在这块空间内,把原来的“a”覆盖了,sb的指向没变
//在这个情况下,由于从始至终只用到了一块存储空间,所以sb和tb的效果实际上是相同的

mutable 优点:

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

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

值的改变 & 引用的改变

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

4 表示泄露和防御式拷贝(defensive copies)

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

return new Date(groundhogAnswer.getTime());
  • 大部分时候该拷贝不会被客户端修改,可能造成大量的内存浪费
  • 如果使用不可变类型,则节省了频繁复制的代价
  • 如果有多个引用(别名),使用可变类型就非常不安全,应安全的使用可变类型:局部变量,不会涉及共享;只有一个引用

5 Snapshot diagram—运行时、代码层面、瞬时

基本类型的值

对象类型的值
可变对象:单线圈

不可变对象:双线圈

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

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

例:用Snapshot表示String和StringBuilder的区别

集合类Snapshot图

  • List

  • Set

  • Map

6  复杂数据类型

Array:
数组是另一种类型的固定长度序列
定义:int[] a = new int[100];
操作:索引、赋值、求长度

List:
列表是另一种类型的可变长度序列
定义:List list = new ArrayList();
操作:索引(get)、赋值(set)、求长度(size)
List是一个接口,其成员必须是对象类型

Set:
集合是零个或多个唯一对象的无序集合
操作:
s1.contains(e):查询是否包含该元素
s1.containsAll(s2):是否s2是s1的子集
s1.removeAll(s2):把s1中所有s2的元素移除
Set是一个抽象接口

Map:
映射是一个键-值对目录
操作:
m.put(k,v):添加
m.get(k):索引
m.containsKey(k):查询是否包含该键值
m.remove(k):删除对应的键-值对
Map是一个抽象接口

Iterator迭代器:
Iterator是一个可变类型对象,它遍历元素集合并逐个返回元素
方法:
next():可变方法,返回下一个迭代对象
hasNext():检测迭代是否到终点
突变会破坏迭代器,应调用iter.remove()方法

基本类型及其封装对象类型都是不可变的,而Collections类静态方法可以获取可变对象例如List/Set/Map的不可变特性(Collections.unmodifiableList/Set/Map),这种包装器得到的结果是不可变的(只能看),但是这种“不可变”是在运行阶段获得的,编译阶段无法据此进行静态检查,例如编译阶段调用sort方法时Java不会报错

 二、ADT

1 抽象和用户自定义类型

除了编程语言所提供的基本数据类型和对象数据类型,程序员可定义自己的数据类型
传统的类型定义:关注数据的具体表示
数据抽象:由一组操作所刻画的数据类型
抽象类型:强调“作用于数据上的操作”,程序员和client无需关心数据如何具体存储的,只需设计/使用操作即可
ADT是由操作定义的,与其内部如何实现无关

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

四类ADT操作:

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

3 表示独立性(RI)

client使用ADT时无需考虑其内部如何实现,ADT内部表示的变化不应影响外部spec和客户端
除非ADT的操作指明了具体的pre和post-condition,否则不能改变ADT的内部表示——spec规定了 client和implementer之间的契约。

4 测试ADT

测试creators, producers, 和 mutators:调用observers来观察这些operations的结果是否满足spec
测试observers:调用creators, producers, 和 mutators等方法产生或改变对象,来看结果是否正确
风险:如果被依赖的其他方法有错误,可能导致被测试方法的测试结果失效
对测试的不同操作进行划分,分别集中进行测试


5 不变量——Invariant

一个好的ADT有一个重要的性质:它能保持它的不变量恒为真,无论程序运行的状态如何;immutability就是一个典型的“不变量” ,由ADT来负责其不变量,与client端的任何行为无关。不变量可以保持程序的“正确性”,容易发现错误,因为我们总是要假设client有“恶意”破坏ADT的不变量

表示暴露:class之外的代码可以直接访问到class中的属性表示
不仅影响不变性,也影响了表示独立性:无法在不影响客户端的情况下改变其内部表示
因此,我们可以使用private阻止外部访问,用final防止再赋值

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

解决方案:
不要将可变参数合并到对象中,创建防御副本
返回可变属性的防御性副本或不可变化的对象
真正的教训——使用不可变属性,以消除防御性复制的需要

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

抽象值构成的空间(抽象空间):客户端看到和使用的值
程序内部用来表示抽象值的空间(表示空间):程序内部的值

Mapping:满射、未必单射(未必双射)
ADT开发者关注表示空间R,client关注抽象空间A

抽象函数(AF):

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

表示不变性(RI):

  • 某个具体的“表示”是否是“合法的”
  • 所有表示值的一个子集,包含了所有合法的表示值
  • 一个条件,描述了什么是“合法”的表示值

检查RI:

  • 随时检查RI是否满足
  • 在所有可能改变rep的方法内都要检查
  • Observer方法可以不用,但建议也要检查,以防止你的“万一”

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

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

ADT的规约里只能使用client可见的内容来撰写,包括参数、返回值、异常等。如果规约里需要提及“值”,只能使用A空间中的“值”。 ADT的规约里也不应谈及任何内部表示的细节,以及R空间中的任何值;ADT的内部表示(私有属性)对外部都应严格不可见;故在代码中以注释的形式写出AF和RI而不能在Javadoc文档中,防止被外部看到而破坏表示独立性/信息隐藏

不变量的建立:
在对象的初始状态不变量为true,在对象发生变化时,不变量也要为true。换言之,构造器和生产器在创建对象时要确保不变量为true、变值器和观察器在执行时必须保持不变性、在每个方法return之前,用checkRep()检查不变量是否得以保持

表示泄漏的风险:一旦泄露,ADT内部表示可能会在程序的任何位置发生改变(而不是限制在ADT内部),从而无法确保ADT的不变量是否能够始终保持为true

一个好的ADT保持它自己的不变量。不变量必须由构造器和生产器建立,并由观察器和变值器保存。
 

三、OOP

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

接口:确定ADT的Spec;类:实现ADT

Concrete class --> Abstract Class --> Interface

接口:

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

也可以不需要接口直接使用类作为ADT,既有ADT定义也有ADT实现
实际中更倾向于使用接口来定义变量,这样打破了抽象边界,接口定义中没有包含constructor,也无法保证所有实现类中都包含了同样名字的constructor。 故客户端需要知道该接口的某个具体实现类的名字,我们推荐使用工厂方法来提供实例对象

抽象类:

  • 至少有一个抽象方法
  • 抽象方法 Abstract Method——未被实现
  • 如果某些操作是所有子类型都共有,但彼此有差别,可以在父类型中设计抽象方法,在各子类型中重写@Override
  • 接口:只有抽象方法的抽象类
    如果某些操作是所有子类型都共有,但彼此有差别,可以在父类型中设计抽象方法,在各子类型中重写;所有子类型完全相同的操作,放在父类型中实现,子类型中无需重写。有些子类型有而其他子类型无的操作,不要在父类型中定义和实现,而应在特定子类型中实现

    protected修饰的属性在子类中可以访问

具体类:

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

2 继承(Inheritance)& 重写(Override)

类 & 类:继承(extends)
类 & 接口:实现、扩展(implement)

覆盖/重写Override

  • 重写的函数:完全同样的signature
  • 实际执行时调用哪个方法,运行时决定
  • 重写的时候,不要改变原方法的本意(Spec)
  • 运行阶段进行动态检查

父类型中的被重写函数体

  • 不为空:

该方法是可以被直接复用的
对某些子类型来说,有特殊性,可重写父类型中的函数,实现自己的特殊要求

  • 为空:

其所有子类型都需要这个功能
但各有差异,没有共性,在每个子类中均需要重写


super

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

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

3 多态(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)
  • 可以在同一个类内重载,也可在子类中重载

参数化多态:使用泛型?编程

当一个方法一致地在一个类型范围<L>上运行时得到的参数化多态。这些类型通常表现出一些共同的结构,它有能力以通用的方式定义方法和类型,这样就能在运行时传递参数,并允许在不完全指定类型的情况下进行静态类型转换,我们将其称为泛型。
泛型编程是一种编程风格,其中数据类型和方法按照要指定的类型编写——之后在需要时实例化时参数提供再特定类型,这样使得程序更具有泛用性。

例如Java中的Set类,它是一组其他类型的元素组成的集合,定义的方法都使用了泛型。
泛型接口,可以有非泛型的实现类,也可以搭配使用泛型实现类。

通配符?只能在使用泛型的时候出现,不能在定义中出现:
List<?> list = new ArrayList();
List<? extends Animal>
List<? super Animal>
泛型在运行时会被擦除,且不存在泛型数组(编译不通过)
Pair[] foo = new Pair[42];

 

子类型多态:

  • 继承:每个类只能继承一个父类,但可以实现多个接口。“B是A的子类”意味着“每个B都是A,B满足A的规约”,并且B要实现A中声明过的每个方法,子类型的规约不能弱化父类的规约。
  • 子类型多态:不同类型的对象可以统一的处理而无需区分,每个对象依据自己的实际类型进行不同的行为,从而隔离了变化。

LSP灵活可替换原则:如果S是T的子类型,那么类型T的对象可以替换为类型S的对象(即类型T的对象可以替换为类型S的任何对象),而不改变T的任何期望属性。

instanceof关键字可以检测前者是否为后者类。
注意:不要把一个父类对象强转成子类,会报错

 

4 一些重要的超类Object方法

equals():
传入一个Object对象,用instanceof动态检查是否为比较对象的类型,不是就返回false;否则将传入对象强转,逐一比较对象的各个属性,都相等再返回true

hashCode():
不同类型属性的重写方法:

@Override
public int hashCode() {
int result = 17;
result = 37 * result + name.hashCode();
result = 37 * result + age;
result = 37 * result + (int)(friendNumber^(friendNumber>>32));
result = 37 * result + Float.floatToIntBits(cash);
result = 37 * result + (int)(Double.doubleToLongBits(wealth)^(Double.doubleToLongBits(wealth)>>32));
result = 37 * result + (isMarry ? 1 : 0);
return result;
}

toString() 每个ADT可自行Override以达到想要的输出效果

5 一个好的Class

immutable类型类的优势:

  • 简单
  • 线程安全
  • 自由分享
  • 无需防御式拷贝

构建方法:

  • 不提供变值器,确保方法不会被重写,所有属性设置为private、final,防止表示泄露,重写equals、hashCode、toString等方法。
  • 能写成不可变类就写,需要可变类型就把可变性减到最小。

四、ADT与OOP的等价性

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

==

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

equals()

  • 对象等价性
  • 对于:对象类型

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

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

2 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;
    }
1


instanceof:

判断类
仅在equals里使用

hashCode():

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

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

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

对可变类型来说,往往倾向于实现严格的观察等价性。但在有些时候,观察等价性可能导致bug,甚至可能破坏RI
可变对象在发生改变时,其哈希值也会随之改变,自身将被放入另一个哈希桶中,这可能会导致一些查找操作的失败。因此,如果某个可变对象包含在Set集合类中,当其发生改变后,集合类的行为将变得不确定。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值