HIT 软件构造 第三章总结

前言

大致复习完这一章的所有知识内容,做了些概括,结合实验中的体会,这章主要讲到了可变和不可变数据类型,讲到了spec(规约),还有ADT(抽象数据类型),面向对象编程(OOP),还有多态,写的内容主要是我自己的一些理解和感悟,可能有不尽完善不够深入的地方,望多多指教。

可变数据类型和不可变数据类型

介绍可变数据类型和不可变数据类型之前,先介绍一下java不像c语言有指针,但其实很多实现都是和指针类似的。一个别名a指向了一个地址,如果这个地址中的数据不能被改变,那就是不可变的(immutable)。比如一些基础类,String,int等都是不可变的数据类型。那么像集合类List,Set,Map这种就属于可变的数据类型,也就是其中内部的数据是可以改变的,就叫可变数据类型。
使用可变数据类型的危害
最大的危害就是因为它可变,所以容易在不改变引用的前提在改变它的值。特别是如果我们返回内部的数据给用户时,如果这个数据是可变的数据类型,那么客户端就有可能恶意修改这个数据。
所以我们要尽量避免使用可变的数据类型
但其实并不是完全不使用,而是在没有必要的场合不使用

如果必须使用,那么首先要注意防御式拷贝,就是返回一个新的对象给客户端,也就是给一个克隆的对象而不是真正的对象。

这一节中还介绍了Snapshot
Snapshot用于描述程序运行时的内部状态。就是各个数据类型的状态,指向的地址内容。
比如一个String a = “abc”,就可以表示为下图:

在这里插入图片描述
由于String是不可变的数据类型,所以用双线圈表示其地址内容。
若令 a = “abcd”,就变成如下形式
在这里插入图片描述
如果a是个final关键字的对象呢?比如final String a = “abcde”,那就将单线箭头改成双线箭头即可。
如果是一个可变的数据类型,Point a = new Point(1,2)
在这里插入图片描述
比较容易将引用不变性和不可变弄混淆
引用不变性指的是一个对象a,如果它已经指向一个地址,那么它将不能被改变指向其他地址。这和它可不可变没有关系,相当于加上了final关键字。

Designing specification

首先我们要了解方法之间如何判断等价。
我们使用行为等价性来判断两个方法是否是等价的。从客户端的角度来看,是根据规约判断行为等价性的。
specification也简称spec,我们叫它规约,它是客户和开发者之间的一种约定,它即约束了客户端,也约束了开发者。

  1. 前置条件(precondition):对客户端的约束,在使用方法时必须满足的条件
  2. 后置条件(postcondition):对开发者的约束,方法结束时必须满足的条件
    如果前置条件被满足,那么后置条件就一定要满足,若前置条件被违反,从理论上讲程序员可以返回任何结果。但是根据程序员的职业道德,开发者有义务尽早提醒用户
    就想合同协议一样的,用户在使用方法时必须要遵循前置条件,开发者也必须前置条件下满足后置条件,实现相应的功能。

spec中有三种关键字

  1. @param:参数关键字,用来描述参数的信息,约束条件等
  2. @return:返回值关键字,用来描述返回值的情况
  3. @throws:异常关键字 ,用来描述抛出异常的情况

根据比较spec,我们可以用不同方法实现相同功能从而进行方法的替换
有着更强spec的方法,可以替换更弱spec的方法。
spec的强度比较:S1>S2
一:S1的前置条件应当弱于S2的前置条件
二:在二者前置条件相同的基础之上,S1的后置应该强于S2的后置
换言之:对客户端的约束条件更少,对开发者的约束条件更多的spec更强
我们在写方法前,应该先构想好spec,然后再根据spec来编写代码,最后还应该检查写出来的代码有没有违法spec。
设计spec的时候还需要注意以下几个条件

  1. 内聚的,spec描述的功能尽量单一,易理解
  2. 信息丰富的
  3. 不易太强也不易太弱
  4. 尽量使用抽象数据类型
    5.有必要时检查前置条件
    6.不能暴露方法内部的变量或实现细节

ADT(抽象数据类型)

ADT分两种:1.可变的数据类型 2.不可变的数据类型
可变的数据类型:一定有方法可以改变其内部数据的值
不可变的数据类型:内部数据不可变,无方法提供修改
建立一个ADT需要的几种方法:

  1. creator 构造器:构造一个数据类型
  2. producers 生产器:返回一个新的数据类型
  3. observers 观察器:观察ADT内部的数据
  4. mutator 变值器:改变ADT内部的数据,只有可变的数据类型才有

比较容易弄混淆的就是producer和mutator,他两之间最大的区别就在于producer并没有改变原数据结构内部的值,而是造了一个新的数据结构,mutator是改变了原数据结构的值。

ADT中有方法,元素属性(rep),之前已经讲了怎么写spec就是针对方法的,针对rep也有相应的注释,就是AF(Abstraction Function)和RI(Rep Invariant)。
AF:R->A 通过字面意思,就是抽象函数的意思,放在映射的情景中,就是ADT中的Rep在表示空间®向抽象空间(E)的映射。即如何去解释R中的每一个值为A中的每一个值。比如Point中的x映射到抽象空间,在客户端看来是横坐标的意思。、
RI:R->boolean ,表示不变性,用来衡量某个具体的表示(R)是否是合法的,也可以看做是对Rep的约束,比如Person类中sex只能是male或者female,这就是对Rep的约束。

不管是AF,RI还是spec,都可以看成一种约束,它规范了我们设计的ADT。
在设计ADT的时候,我们一般都是使用面向对象的编程思想,对一个事物进行剖析,常用的一种思考方法就是名词分析法,这个东西有什么特征,属性,这些名词都能作为我们设计的Rep,方法。我们在设计的时候一定要保证RI和AF的正确性,不能违反。所以我们在确定设计一个ADT的时候,应先把其AF和RI写出来,再根据AF和RI来确定方法的spec,最容易犯的错误就是写出来的方法可能违反了RI和AF。为什么一定要严格遵守RI和AF呢,在后续的学习中还有学习到LSP原则,RI和AF在LSP原则中至关重要。

既然要保证RI和AF的准确性,那我们就在对Rep变化的地方都进行检查,也就是CheckRep()。
CheckRep():一种用来检查RI的方法,里面就是对Rep的检查,比如上面说的性别的检查

checkRep(){
	assert sex.equals("male") || sex.equals("famale") : "性别不对";
}

我们有必要在creator,producer,mutator方法中都添加checkRep方法进行检查,只有这样才能真正保证我们写的方法是满足RI和AF规定的。

面向对象编程(OOP)

在学c++的时候就有过面向对象的方法进行编程,其实java里也有相同的概念。和c++中的类定义类似,在java中ADT就是一个类,所以在设计ADT的时候,我们也按面向对象的思想编程。
那么java中的OOP有什么特点呢?

  1. 封装与信息隐藏
  2. 继承和重写
  3. 多态,子类型,重载
  4. 静态和动态分派

interface(接口)
在进行ADT设计的时候,我们可以提前定义ADT中要实现的方法,在写具体的实现,这就是接口(interface)和类(class)。
接口里就是对类的实现先进行定义,类就是对接口的方法进行实现,所以class和interface之间的关键字是implement(实现)。
接口和类类似,类可以继承,接口自然也能,在java中接口与接口之间的继承关系叫做扩展extends。
同样的,一个类可以实现多个接口(从而就可以实现多个接口中的方法),一个接口也可以有多种实现类。这一特性就能让我们在接口和实现类中间进行多样的组合,编程思想也变得丰富。

interface具体是什么呢?
可以将interface和抽象类来想,interface就是定义了一堆方法,但没有具体的实现,它起到一种封装的效果,即正是因为它没有具体的实现,所以客户端在使用这个接口的时候就不知道具体的实现过程,只能通过各个spec来了解各个方法实现了什么功能,这就是信息隐藏,当然,要想使得接口能够使用,自然要有实现类。
比如List是一个接口,它的实现类就有ArrayList,LinkedList等等。当时不管是ArrayList还是LinkedList,在使用接口对象对其声明的时候它们都能实现相同的功能,所以接口使得我们实现的手段变得多样。

那么接口里的方法都没有实现体吗?
并不是的。使用static和default关键字的方法是可以有实现体的。前者叫静态方法,后者是默认方法。静态方法不需要申请一个具体的实现类就可以使用,只不过要在方法前加上接口名,default也是一样的,一旦使用了static关键字,那么这个方法在实现类里面就不能再重写了,但是default是可以被重写的,它出现的意义就是可以在接口中写方法的实体。

实现类
实现类就是extends(扩展)了接口的类,那么这个类就有了接口里所有定义的方法,它要对这些方法进行重写(@Override)。实现类中可以添加新的方法,但这些在接口中没有的方法,在接口使用的时候是用不了的,可以说是类的个性。

重写的概念在十分重要,无论在c++还是在java中,重写都是多态的一种重要手段。
重写在继承关系中十分常见,子类型可以重写父类型中的方法,和父类型有不同的实现,但是如果严重要求spec原则的话,子类型重写的方法应当和父类型中的方法一样满足同一个spec,不然就不能用子类型来替换父类型,在接口中也是一样的道理。
重写的要求:重写的方法和父类型中的方法应该满足参数和返回值类型一致,并且遵循同一个spec。
重写是在编程中十分常见的手段,因为继承的出现,子类型有着与父类型不尽相同的特性,所以有些方法实现起来就不一样了,特别是equals()和hashcode()函数,我们需要通过重写,完成对spec的遵守,无论是改进,还是多态,重写发挥着十分重要的作用。

多态(Polymorphism)
什么是多态?同一个方法不同的实现手段?同一个方法名不同的参数功能?还是不同的参数都能用同一个方法?这些都是多态。
多态分以下几种:

  • 特殊多态:overload(重载)
  • 参数化多态:泛型
  • 子类型多态:不同类型的对象可以同一处理而无需区分

Overload
重载,前面有一个重写,重载和重写都是实现多态的重要手段,但用起来却不一样。
重载是在相同方法名的基础上,使用不同的参数或返回值类型。就行人一样的,完全不相同的两人名字一样,比如一个类有不同的构造函数,这就是一种重载。
重载一定有是参数列表不同,它的价值就在于用户使用不同的参数,也可以调用相同的方法。
下图详细的比较了重载和重写的区别:
在这里插入图片描述
最后一行也可以看到,重载在静态类型检查的时候就已经确认了,而重写则是在动态检查的时候才确认的,之所以能在静态阶段就能确定就是因为编译器是通过参数列表来判断调动那个方法的。

参数化多态
在java中,参数化多态就是泛型(generics)。
泛型的重要意义在于它可以代表任何的数据类型,我们使用泛型编程的时候固然可能会受到一定的约束,比如个性的方法不能调用,但是使用泛型可以使得我们编写的代码适用于任何的数据类型,大大提高了适用范围,比如List中的E就是泛型,所以我们无论是用String,int还是自定义的类型都可以使用List。
还有一个和泛型相似的东西,通配符(?),就是一个问号,他们有什么区别和具体使用的场景还是有很多讲的,这里就不赘述了,有兴趣的小伙伴可以去其他大佬的博客转转。

子类型多态
这是一个非常重要的概念,一个接口有不同的实现类,是一种子类型多态,一个类有多个子类,是一种子类型多态,接口与接口的扩展,类与类的扩展,不同对象,可以统一的处理而无需区分,子类型多态千变万化,在LSP原则下,我们可以肆意的对接口和类进行各种各样的组装,以达到我们想要的效果。这里会在第四章重点提到。

静态和动态分派
绑定:将调用的名字与实际的方法名字联系起来(可能有多个,比如重载和重写)
分派:具体执行哪个方法
静态分派:编译阶段即可确定要执行哪个具体操作
动态分派:编译阶段可能绑定到多态操作,运行阶段决定具体执行哪个(overload和override均是如此)
推迟绑定:编译阶段不知道类型,一定是动态分派(override是推迟绑定,override是early binding)

equality in ADT and OOP

判断等价性:

  • 从用户的角度,AF相同
  • 从方法的角度,任何方法都得到相同的结果

代码中判断两个数据类型是否相等:

  • equals:对象等价性(自反,传递,对称)
  • ==:引用等价性(内存地址相同)

Object缺省equals:使用==判断,其实还是根据内存地址判断。
但在我们具体使用两个数据类型的时候,不可能都根据两个类型内存地址来判断相等的,肯定有它的判断依据,所以一般而言都需要尽可能的重写equals。
一旦重写了equals,那就必须得重写hashcode,如果不重写,可能可以判断正确,但是也可能无法判断,比如在集合类中,如果用了HashSet或HashMap,没有重写hashcode的话,对于可变的数据类型,那么其hash值发生变化,contains方法就用不了了。

说到可变和不可变,他两的等价性判断也是有区别的。
对可变的数据类型来说,等价性分两种:

  • 观察等价性:在不改变的情况下,调用observer方法结果一样
  • 行为等价性:调用对等的任何方法都展示出一致结果
    可变数据类型往往实现严格的观察等价性,但有时候,观察等价性可能导致bug,甚至破坏RI。
    所以,对于ADT,要根据实际需求来确定使用哪种等价性。
    在基本数据类型中,使用==这种引用等价性就可以了,但要注意常量池。
    比如String a = new String(“a”) 和 String b = “a”;使用引用等价性肯定判断不了相等,因为一个在堆中分配空间,一个则是在内存中。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值