软件构造复习笔记【四】 (第6、7章)


这次笔记我们主要学习第四部分。

一、抽象数据类型(ADT)

1.1 抽象数据类型的四种操作

构造器(creator),输入一些其它类型的对象,创建一个该ADT对象。例如创建一个新集合new()或者现实中的构造函数。
简单来说就是new、构造函数。
生产器(producer),通过该ADT的旧对象,创建一个该ADT的新对象,例如计算当前集合与S的交集的方法ins(S)。
简单来说是返回一个相同类型的对象。
观察器(observer),通过该ADT本身的数据以及传入参数,计算得到其它类型的值。例如检查集合里是否有x的方法find(x)。
简单来说就是get函数等。
变值器(mutator),作出“修改ADT内部数据”的行为,是可变对象与不可变对象的本质区别!例如将x加入集合并返回加入x后的集合大小的方法add(x)。
改变抽象类的值,要注意返回值不一定是void类型。
在这里插入图片描述

图1.1 抽象数据类型的四种操作

测试creators, producers, and mutators可以调用observers来观察这些operations的结果是否满足spec。
测试observers:调用creators, producers, and mutators等方法产生或改变对象,来看结果是否正确。

1.2 AF/RI/rep

首先我们需要了解一下这些都是什么含义。
在此之前,要知道表示空间R和抽象空间A的关系。

1.2.1 R和A

表示空间R代表开发人员看到的内容,抽象空间A代表用户能看到的内容,两者的映射如下:
在这里插入图片描述

图1.1 R和A的映射

其中rep代表内部表示属性

1.2.1 AF

AF(抽象函数)即是描述从R到A的映射关系的函数,即如何将表示空间中的一个值解释为抽象空间中的一个值。
AF必然是一个满射,同时它不一定是单射(必须没有非法的表示值才能说是必定满射)。
表示方法:
AF(rep)=rep在类中的含义。

1.2.2 RI

RI(表示不变量)告诉了我们表示空间R中的值是否能被映射到抽象空间A中,也可以认为RI构成了表示空间R的一个子集,该子集中的值能够被映射到抽象空间A中。
简单来说,表示不变量告诉了我们变量是否合法。

1.2.3 构建ADT的步骤

在构建ADT时,我们需要有以下步骤:

  1. 选定表示空间(R)
  2. 进而找出其中满足条件的子集(RI)
  3. 并为子集中的每个元素做出对应的解释(AF)
  4. 最终将其映射到抽象空间(A)中。

1.2.4 表示独立性(Representation Independence)

表示独立性的概念:客户端在使用ADT的时候无需考虑其内部是如何实现的,ADT内部标识的变化不影响外部Spec和客户端。即ADT的内部实现变化不应该影响用户在客户端的使用。表示独立性的关键在于将数据结构的使用和数据结构自身的形式分离。防止因为用户在使用过程中假设ADT内部的实现,在假设的基础上形成依赖。所以ADT的属性要是private形式。

1.2.5 表示泄露(Representation Exposure)

如果ADT不幸地让客户端得到了自己内部表示(可变对象)的引用,那么客户端就可以不通过ADT的操作,而可以通过非法后门修改ADT的内部表示,产生表示泄露。
如何避免表示泄露?

  1. 使用private和final关键词对域进行修饰。
  2. 使用防御性拷贝,需要注意的是,防御性拷贝可以发生在传入和传出数据时。
  3. 通过规约对用户的行为进行限制。
  4. 使用不可变类型的数据构建ADT。

其中防御式拷贝例子如下:

return new HashSet<>(set);

记得可变类型的传入与返回用上new

1.2.6 以注释的形式撰写AF/RI

1.2.6.1 怎么写

不同的内部表示,需要设计不同的AF和RI
AF和RI都应该记录在代码中,紧挨着rep的声明。首先选择某种特定的表示方式R,进而指定某个子集是“合法”的(RI),并为该子集中的每个值做出“解释”(AF)——即如何映射到抽象空间中的值。
同样的表示空间R,可以有不同的RI
即使是相同的R、同样的RI,也可以有不同的AF,即“解释不同”
撰写的例子如下:
在这里插入图片描述

图1.2 写的例子

其中RI、AF也可以用汉字书写。

1.2.6.2 注意的点

设计ADT过程:

  1. 选择R和A;
  2. RI——合法的表示值
  3. 如何解释合法的表示值——映射AF

随时检查RI是否满足要用checkRep()

有益的可变性(Beneficent mutation):我们可以采取改变R值而不改变A值的操作,使得抽象类的rep发生变化,使得实现过程更加自由,这就是有益的可变性。但这并不代表在immutable的类中就可以随意出现mutator。

二、面向对象的编程

2.1 接口,抽象类,具体类

抽象类:具有抽象方法的类;抽象类不能实例化。一个类至少包含一个抽象方法。
抽象接口:只含有抽象方法的抽象类。
具体类:ADT中的所有方法都必须实现。一个类可以实现多个接口。
接口中的属性默认是public static final的,不能具有构造方法,也要符合ADT中的条件。
接口中的构造函数可以通过static编写的工厂方法实现,用于在接口中直接创建一个示例。
在接口中添加方法可以用default方法实现。
总的来说,接口用于规定方法,而类用于实现方法。

2.2 继承,override

这一部分是重点内容。
继承(extend)发生在类与类之间。

2.2.1 重写(override)

我们通过继承得到的子类中,方法可以大体分为两类,一类是可以进行重写的方法,另一类就是严格继承下来的方法。区别就在于一个可以进行override,而另一个不可以override。而对于严格继承的方法,在Java中以是那些加了final修饰符的方法。使用final修饰的方法就不能在子类中进行重写。
子类可以通过super()函数直接使用父类的方法。
调用父类的构造方法,写法如下:

public class Student extends Person {
    public Student(String name, int age, String birth) {
        super(name, age); // 调用父类中含有2个参数的构造方法
    }

调用父类的方法,写法如下:

    void message() {
        System.out.println("This is student class");
    }
    void display() {
        message();
        super.message();
    }

我们在进行对父类的重写的时候在格式上需要注意几点:
两个方法的签名要保持完全一致:在方法前面加上@Override
声明部分直接复制粘贴;
可见性要变得更强或者是相等。

对于那些所有子类型完全相同的操作,放在父类型中实现,子类型中无需重写。如果某些操作是所有子类型都共有,但彼此有差别,可以在父类型中设计抽象方法,在各子类型中重写。而有些子类型有而其他子类型无的操作,不要在父类型中定义和实现,而应在特定子类型中实现。

所有引用类型的最终祖先类都是Object,都具有等价性判断equals()、哈希函数hashCode()和字符串转换toString()方法,可以重写它们。
IntegerDouble等表示数值的引用类型都继承自Number类,都具有转换为其它数值类型的方法(例如intValue())。
所有异常类都继承自Exception,对于父异常的catch能够捕获子异常。

2.3 多态、overload

2.3.1 多态的种类

特殊多态
一个方法可以有好几个同名的实现方式,在很多编程语言中,特殊多态被一种叫方法重载的形式支持。
参数化多态
代码的参数不被限制于特定的数据类型而进行的编程,也就是说一个类型名字可以代表多个类型。参数化多态更为熟知的叫法叫泛型编程。
子类型多态
一个变量的名字可以代表多个类的实例。

2.3.2 重载(overload)

方法重载(属于特殊多态)
多个方法具有同样的名字,但是有不同的参数列表或者返回值类型。
方法重载这个功能是一种能够实现同名但是不同实现函数的能力。对重载函数的调用结合调用这个函数的上下文,来完成特定的实现。也就是允许一个函数根据上下文来执行不同的任务。
简单来说,重载就是两个方法共用同一个名字,但是必须要有参数上的不同,如个数、顺序等。
注意:只改变参数名不改变类型不算改变参数列表!
方法的重写是一种静态多态,即要执行哪一种方法,是编译时期决定的。

2.3.3 重写与重载的对比

在这里插入图片描述

图2.1 对比图

另外,重写的方法是在运行时进行动态检查;重载的方法是在编译时进行静态检查。

2.4 泛型

泛型(属于参数化多态)
泛型能够将类型作为类/方法的参数,使得操作与类型无关。
使用泛型变量的三种形式:泛型类、泛型接口和泛型方法。其中泛型方法为选学,此处不加赘述。

2.4.1 泛型类

如果一个类中声明了一个或者多个泛型变量,则成为泛型类,这些类型变量称为类的类型参数。在这个类中可以定义一个或者多个类型变量作为参数来使用,所有已经被参数化的类型在同一个类的运行时都保持同一种类型。
简单来说,类中的泛型属性都被在调用时都会被改成对应的类型(编译时更改)。如果要将程序改成泛型,只要将对应变量和类名中加上<E>即可。若需要改掉多个变量,则需要用不同的字母。

2.4.2 泛型接口

如果一个接口声明了一个或多个类型变量,那么它就是泛型的。

  • 这些类型变量称为接口的类型参数。
  • 它定义了一个或多个作为参数的类型变量。
  • 通用接口声明定义了一组类型,每个类型参数部分的可能调用都有一个类型。
  • 所有参数化类型在运行时共享相同的接口。
    java中常用的set就是一个泛型接口。

2.4.3 通配符

这东西比较迷惑,它分为三种:
无边界的通配符<?>,固定上边界的通配符<? extend E>,固定下边界的通配符<? super E>
一般在传参时使用,用于传入未知类型数据。固定上边界的通配符用于限定传入参数为E本身及其子类,固定下边界的通配符用于限定传入参数为E本身及其父类。

2.5 子类型多态

子类型多态:期望不同类型的对象可以统一处理而无需区分,遵循LSP原则,关于LSP将会在第九讲介绍。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值