软件构造随笔5

复习中

1.ADT
• 所谓ADT-抽象数据类型,强调作用于数据之上的操作,并不关心数据具体是怎么存储的。例如
我们的字母集合,只对它定义了操作,它的元素到底是怎么存的?用户不需要知道。可以是字
符串,可以是字符数组,可以是哈希表,可以是红黑树……
• ADT可以有4种操作:
• ①构造器creator,输入一些其它类型的对象,创建一个该ADT对象。例如创建一个新集合new()
或者现实中的构造函数。
• ②生产器producer,通过该ADT的旧对象,创建一个该ADT的新对象,例如计算当前集合与S的
交集的方法ins(S)。 • ③观察器observer,通过该ADT本身的数据以及传入参数,计算得到其它类型的值。例如检查
集合里是否有x的方法find(x)。 • ④变值器mutator,作出“修改ADT内部数据”的行为,是可变对象与不可变对象的本质区别!例
如将x加入集合并返回加入x后的集合大小的方法add(x)。
变值器不一定返回void!
creator
mutator
producer
observer
注意非静态方法(构造函数除外)
的输入参数还会隐含一个当前
对象!
例如:public static
WordList empty(); 这个静态
方法就是一个creator,输入参
数里没有该类型对象
2.类与接口
• 接口相当于规定了ADT所需的未实现的操作(方法),这是用户所关注的。
• 类真正地在代码层面实现了接口规定的ADT操作,并且实现了ADT内部的数据存储。
客户端代码: 真正的对象实例
通过接口调用实例类型中实现的方法
静态检查错误!
接口没给你提
供这个方法!
动态检查错误!set在运行时
指向StringSet对象实例,
无法转换为ArraySet!
运行时可以转换,set本来
就指向ArraySet的对象实例
@Override可加可不加,
编译器会自动得知这个方
法是接口定义的实现
类的构造函数,相当
于ADT操作new()
通过new调用具体的类的构造函数,创建具体类的实例对象
通过接口声明变量,让它指向这个具体类的实例对象
不管具体实例是什么类型的,客户端都可以通过接口调用统一的操作!
相当于,接口将具体类实现的ADT操作开放给了客户端。
不加默认为public
可同时实现
多个接口
这个方法无法
通过接口调用
2.类与接口
• 接口实际上可以通过default或者static直接编写方法实现。
• 例1.我不希望客户端得知具体类名,但客户端需要创建实例对象。基于static的工厂方法
• 例2.我突然想为字母集合ADT增加一个获取集合大小的方法size(),但又不希望修改已经实
现的具体类。在接口中就已实现默认操作的default方法
• 客户端代码:
当然,具体
类也可以重
新实现size
方法
客户端不需要知道具体
类名,尽管这是一个
StringSet的实例对象
一个接口可以通过extends继承另一个甚至
多个接口的方法,但显然不能implements
final方法用于类的继承,
用在接口里也没有意义啊
接口中的方法原则上是不应该private的,毕竟
接口要把类定义的方法公开给客户端,但Java9
开始可以,不过必须在接口中给出具体实现
3.方法规约(spec)
给人看的注释,需要
人为检查是否满足
规约是客户端与实现者之间签订的“契约”,客户
端的输入应当满足前置条件,实现者编写的程序
应当给出满足后置条件的结果。
规约描述了方法的功能以及接口(“能做什么”),
不需要依赖(也不应该透露)方法的具体实现。
给编译器看的方法声明,能够对方
法接口的调用进行静态检查
前置条件
(参数要求)
后置条件(返回
值要求,或者
throws抛出的
异常)
ADT中的方法规约在接口
CharSet中就应该声明,
不应该跟具体实现有关
对于这个add方法,如果客户端故意
输入一个不是字母的x,违反前置条
件,那么add方法也就不需要满足后
置条件,理论上干什么都行
更强的规约:前置条件更弱,后置条件更强,
满足更强规约的方法一定能替代满足更弱规约
的功能,客户端肯定更喜欢强规约方法(有更
大的自由度),但这增加了实现者的压力……
(例:右边的add的规约强于左边的)
·右边的add
·左边的add
规约图:更强的规约表示为更小
的区域(因为满足它的方法更少)
对于外圈,客户
端必须传入字母
对于内圈,客户端
可以传入任意字符
客户端更容易满足 甚至还能达到更好的效果
3.方法规约(spec)
前置条件更弱了,后置条件更强了(抛出的异常更少了),规
约更强了,客户端自由度更高,方法更难实现
4.AF/RI/rep
• 对于ADT,客户端是无法看到其内部表示属性(rep)的,客户端只会看到ADT在表面上展现出来
的东西,即,ADT做了一个由表示空间®到抽象空间(A)的映射AF。 • 不是所有表示属性rep都能映射到相应的抽象值的(即有一些rep是非法的),那么任何时刻
ADT的rep都必须满足一定规则(合法),即表示不变量RI。 • 有时应该引入一个方法checkRep检查rep是否满足RI,像这样:
应该以注释形
式写出AF和RI
给客户端展现出的是一
个字母集合(抽象值),
然而实际上的数据是一
个字符串(表示值),
ADT正是使用操作们对
这个字符串做了特殊的
“解读”,实现了AF
其它可能的RI和AF:
要想满足这个RI可能
还要修改add的规约
要想完善这个AF可能还要增加
“统计某字符出现次数”的方法
客户端可以看到ADT的操作方法及
规约,可以看到抽象空间以及抽象
值,但就是不能看到rep、RI以及
AF,它们涉及到客户端不应该知道
的与内部实现有关的细节 这个例子中多个rep可
以映射到同样的抽象值 一个便于理解但不太准确的例子,对于黑盒函数 𝒇 𝒙 = 𝐥𝐧𝒙 ,rep就是x,
AF就是表达式𝐥𝐧𝒙 ,表示空间和抽象空间都是实数域,RI为x>0的限制
这是一个仅实现者使用的private
内部方法,客户端不需要知道
4.AF/RI/rep
• 注意!对于不可变类型,它的抽象值一定是不能变的,但表示值可以改变(反正客
户端也不知道),前提是改变后通过AF仍然映射到相同的抽象值!
这丝毫不影响该类
是一个不可变类型
4.AF/RI/rep
写RI=写rep满足的规则=“阅读理解”
5.表示独立性与表示泄露
• 表示独立性是指,客户端使用ADT时无需考虑(也不应该知道,更不应该直接访问
到)其内部如何实现,ADT内部表示的变化不应影响外部spec和客户端。
• 如果ADT不幸地让客户端得到了自己内部表示(可变对象)的引用,那么客户端就可
以不通过ADT的操作,而可以通过非法后门修改ADT的内部表示,产生表示泄露。
产生表示泄露!
char[]是mutable的!
“主动泄密”
客户端通过后门篡改了
ArraySet的rep
应该像这样做防御性拷贝!
返回一个新对象的引用
另外一种典型的
“内鬼式”表示泄露,
直接把传入的可变
对象引用赋给rep
客户端通过传入的内鬼
直接在外面改变rep
传入时也应该像这样做防御式拷贝
实现者应该以注释的形式声明如何避免表示泄露:
这是最基本最起码的保护方式
6.继承与重写
• 一个类child可以继承另一个类father的全部方法与属性,在运行时存在继承关系的类型对象可
以互相转换。子类型也可以重写(override)父类的方法,替换为自己的实现代码。
• 例:我希望对类StringSet进行扩展,创建一个可记录操作日志的类StringLogSet
extends指定父类,Java只允许一个父类
getLog被声明为final,
StringLogSet的子类不能再重写它! 使用super指代自己的父类,这
里是先调用父类的构造函数
重写add方法
调用父类定义的add方法
@Override可加可不加
在代码中注明这是重写方法
客户端代码: set尽管表明上是一个StringSet类型的引用变量,
但它实际上指向一个StringLogSet对象
子类自己新增的方法
在运行时发现set实际指向StringLogSet对象,
那么就调用StringLogSet重写的add方法
ins没被重写,调用StringSet的
静态检查错误!set的表面类
型StringSet没getLog方法!
在运行时尝试向下转换到子
类型,若set实际指向的对象
与StringLogSet没有继承关
系,则转换失败,引发异常
现在可以调用logSet
对于一个表面类型为父类的引用变量a,通过a.b()调
用方法时,在运行时将会动态地检查a是否实际指向一
个子类型对象,子类型是否重写了b()方法,如果是那
么则调用子类型的重写方法。这一机制实现了子类型
多态,即同样的a.b(),可能调用不同的方法。
参数列表和
父类的一致
6.继承与重写-“我是什么类型”
• 考虑两个函数func1和func2: • 分别调用它们:
• func1(new StringSet())=666
• func1(new StringLogSet())=233
• func2(new StringSet())=233
• func2(new StringLogSet())=233
s的表面类型是StringSet,这个
编译器在静态检查的时候知道,
但s具体指向的对象到底是什么类
型,编译器并不能知道
不能用instanceof判断一个对象是否是真正的父类对象!
一定要明白,变量在代码
中指定的的表面类型,和
运行时变量实际指向的对
象类型,可能是具有继承
关系的不同类型
运行时instanceof将尝
试将s转换为
StringLogSet,若转换
失败则得到false的结果
总结:Java中常用类的接口实现与继承
• 容器类可以有不同的底层实现,但它们的操作都较为统一,表示为接口实现的形式。例如,
ArrayList和LinkedList都实现了接口List,都能实现列表的统一操作,但分别基于数组
和链表实现。HashMap<K,V>和TreeMap<K,V>都实现了接口Map<K,V>,都能实现映射的统一操作,但
分别基于哈希表和红黑树实现。HashSet和TreeSet都实现了接口Set,都能实现集合的统
一操作,但分别基于哈希表和红黑树实现。
• 所有引用类型的最终祖先类都是Object,都具有等价性判断equals()、哈希函数hashCode()和字符
串转换toString()方法,可以重写它们。
• Integer、Double等表示数值的引用类型(跟int、double这些非引用的内置类型并非一回事!)都继
承自Number类,都具有转换为其它数值类型的方法(例如intValue())。 • 所有异常类都继承自Exception,对于父异常的catch能够捕获子异常。
• 这些Java相关的知识考试的时候可能会默认你知道。
7.ADT的等价性与equals()
• ADT的等价性是对于客户端角度而言的,要么,两个对象通过AF映射到相同的抽象值,要么,两个
对象能做出效果相同的行为。不一定非得让rep完全一致。
• 所有对象都继承了Object.equals(),我们可以在类中重写它,从而定义自己的等价规则。注意
equals()与不是一回事,后者仅仅判断两个引用是否指向同一对象。如果不重写
Object.equals(),那么默认效果和
是一样的。
• 对于可变对象,除了上述的“观察等价性”,还会有一种“行为等价性”,如果两个对象这个时刻等价,
那么不管之后干了什么,这两个可变对象仍然是等价的。(一般和一样,当且仅当是同一对象,
例如StringBuilder,我们常用的除它之外的类一般都是观察等价性,如String、List)
由于equals的参数必须是Object,
所以需要先检查类型是否匹配 注意:这玩意并没有在重写Object.equals,
参数列表都不一样,这是重载的同名方法!
s1
s2 false
sb1sb2 false
sb1
sb3 true
s1.equals(s2) true
sb1.equals(sb2) false
sb1.equals(sb3) true
l1.equals(l2) true
8.hashCode
• Object.hashCode()计算对象的哈希值,将对象映射为一个整数,使得等价的对象具有
相同的哈希值。好的哈希算法应当使得不等价对象的哈希值尽量不同。
• hashCode()一般用在基于哈希表的集合类中(HashMap、HashSet)。查找一个对象时,
先通过hashCode直接找到对象对应的桶,再在桶里一个个用equals比较。
• Object.hashCode()默认直接返回对象的地址,即对象哈希值相同当且仅当为同一个对
象(引用一样),当使用了自定义的等价规则时,需要重写hashCode。
StringSet.hashCode的一种可能的实现
{a,b,d}->(1011)2=11 hashCode的实现一般可以通过进制数的
方式,将不同的组合部分表示为进制数中
的数位,最终得到一个大数,一般可通过
取模使其离散。
(考试应该不会考这些,了解即可)
9.重载
• 重载(overload)机制使得同一个类中的多个方法可以有相同的名字,前提是它们
有长度不同的参数列表,或者对应不同的参数类型,起码,得让编译器在进行静态
检查的时候通过你调用时传入的参数判断实际上应该选择哪个方法。重载也可以发
生在父类与子类之间(子类重载父类的方法)。
func1
func2
true(MyClass.equals)
false(Object.equals)
重载父类的equals,而不是重写!
重载函数的返回值
可以没有任何关系
编译器在编译时选
择合适的方法调用
overload实际上属于特设多态
(ad-hoc),这种多态下,同一操
作针对不同类型表现出不同的行为
(但如果两个重载方法的参数列
表十分接近,编译器决策不了到
底应该调用哪个,静态检查错误)
10.泛型
• 泛型是参数化多态的实现机制,它能够将类型作为类/方法的参数,使得操作与类型无关。
• 例:我们发现为CharSet这个ADT设计的操作实际上可以扩充到任意类型的集合,因此可以将其使用泛型扩展为
Set以及具体类ConcreteSet。
带有类型参数T的接口
1是Integer,Integer继承自Number
静态检查错误!ConcreteSet和
ConcreteSet没有继承关系!(泛型的不可协变性)
泛型不存在于运行时!编译器会为它们自动生成两个真正的类ConcreteSet_Integer和
ConcreteSet_Number,并将类定义中的T分别直接替换为Integer和Number(类型擦除)
3种基本的多态:
①特设多态(同一操作,不同类型不同行为),重载
②参数化多态(操作与类型无关),泛型
③子类型多态(同一对象可能属于多种类型),继承/重写
加extends表示T必须是Number的后代类,同理若加super表示T必须是xxx的祖先类
此时已经告诉编译器限定了T是Number的后代
类,那么T的变量就可以当作Number变量使用
这个带ext

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值