前言:课程逐渐深入,第三章的主要学习目标是攻克ADT和OOP两个在编程中非常重要的概念
Section 1 类型与变量
一.数据类型
在Java中数据类型可分为两种:基本数据类型和对象数据类型。
基本数据类型如int,double,boolean,和C语言中的数据类型类似;对象数据类型是面向对象编程语言中的特殊类型,比如类,接口这些面向对象编程中的概念。下面的表格将二者作了详细比较:
在对容器类型(如list,set)的元素进行操作时,要求元素必须为对象类型,这时基本数据类型可通过包装转换为对象数据类型 。相对应的对象类型为
Boolean, Integer, Short, Long, Character, Float, Double。一般情况下会自动转换
二.类型检查
1.静态类型语言:编译时所有变量的类型已知,在编译阶段进行类型检查的语言
2.动态类型语言:在运行阶段进行类型检查的语言
3.静态检查:在程序运行之前自动检查bug,检查类型而不检查值,避免将bug带入运行阶段,提高健壮性
4.动态检查:程序运行时自动检查bug,是关于“值”的检查
注解:静态类型语言、动态类型语言与静态检查、动态检查之间没有绝对的关系,无论是哪一种语言,都既可以作静态检查又可以作动态检查;
静态检查,动态检查并不是针对某种语言而言的,无论是哪一种语言,它都会在特定时间点进行检查,只是表示一种在特定时间点检查的检查策略
就检查策略而言:静态检查>>动态检查>>无检查
5.可利用静态检查检查出的常见bug:
语法错误、类名错误、函数名错误、参数数目/类型错误、返回值类型错误等等
6.可利用动态检查检查出的常见bug:
非法参数值、除零问题、非法返回值】越界、空指针
三.可变与不可变
1.不变数据类型:一旦创建以后,其值不能被改变;如果是引用类型,一旦确定其指向的对象,不能再被改变指向其他对象,但指向地址空间中的内容还可以改变2.可以用final对变量进行修饰,表明该变量不可变。其特点是,final类无法派生子类;final变量无法改变值/引用; final方法无法被子类重写
如果编译器无法确定final变量不会改变,就提示错误,这也是静态类型检查的一部分。同时 final也表明了程序员的一种“设计决策”
3.可变数据类型:顾名思义,可变对象拥有方法可以修改自己的值或引用
与不可变数据类型有何区别呢?区别主要体现在对一个变量有多个引用时。我们来看一看下面的图:
①对于不可变类型的多个引用:此处是String t和s,假设都指向一个“ab”字符串,此时如果将t后面添加字符串“c”,由于是不可变数据类型,其值不能改变,因此会新建一个对象“abc”,它处于一个新的地址空间,然后t指向这个新的地址空间。由于“ab”的地址未发生改变,s也未进行操作,因此s仍会指向“ab”,此时s和t就是两个不同的引用了
②对于可变类型的多个引用:此处是StringBuilder tb和sb,假设都指向一个“ab”StringBuilder,jiangtb扩展为“abc”,由于其是可变数据类型,因此其引用空间的值是可以改变的,因此这个地址的内容直接由“ab”改为“abc”,而由于tb,sb指向同一个地址空间,因此tb,sb两个引用的值都发生了改变。
③总结起来就是:对于可变数据类型,如果存在多个引用,只要有任意一个引用对值进行改变,所有引用的值全部发生改变,有时这并不是我们所希望的,存在着很大的风险
但不可否认的是可变数据类型也减少了频繁修改带来的大量临时拷贝因此可以提高效率
如何选择就需要看我们编程时注重哪些质量指标,“折中”的策略具体是什么样的
四.代码快照图
代码快照图用来描述程序运行时某一特定时刻的内部状态。对于如何画代码快照图以及如何读代码快照图,以下面的图片作为例子来简单介绍一下
五.复杂数据类型
1.数组:在连续的地址空间内存储相同类型的一系列数据:如int[] a = new int[100];
数组的长度可以是由常量确定,或是某个变量的值来确定,这点与C语言不同,但数组擦很难过度一旦确定,数组的长度就不能再改变
2.List:List list = new ArrayList();用来保存同一数据类型的有序数据。值得注意的是List是一个接口,它有多种实现形式,如ArrayList,LinkedList。接口的概念后面会进行介绍
3.Set:同样是一个接口,与List类似,但保存的数据中没有重复元素,与普通的集合概念相同
4.Map:也是一个接口,保存的都是键值对,键的类型与值的类型可以不同,但应唯一确定
对于2.3.4这种容器类型,加入元素时会进行静态检查以确保加入元素的类型是正确的。基本数据类型在加入时会自动包装为对象类型
5.迭代器:用于遍历元素。迭代器是一个对象,它遍历一组元素并逐个返回元素。
for(…:…)形式的遍历,调用的是被遍历对象所实现的迭代器
Section 2 规约(specification)
一.What is specification
1.规约是一种编程过程中由编程人员记录下来的注释形式的“编程文档”。用来展示编程人员的设计决策。
2. 代码本身就蕴含着设计决策,如你使用了对象数据类型而不是基本数据类型,你将某个类设置为不可变而不是可变,亦或是使用了final关键字等等,但假设另外一个编程人员来分析你的程序,他就要从头分析代码,这是费时且不精确的,因此我们需要将我们的“设计决策”以注释形式记录下来,这样就能够更好地交流
3.规约不同于普通注释,普通注释是//双斜线形式或者/* */形式,规约的形式为
这种形式的注释在处理时会被Java自动提取为文档。
图片上显示的是Java中一个常见的方法规约,首先它先描述该方法的功能是什么,其次以@param开头的是描述方法的参数,参数有哪些限制条件,被我们称为前置条件(requires)。前置条件对输入进行限制,其中不满足条件的输入可不在该方法的考虑范围之内。功能以及@return开头的返回值共同描述了这个方法的后置条件,即该方法产生的效果(effects)
规约就是由前置条件和后置条件两部分组成的。前置条件满足的情况下,后置条件一定要满足。也就是说程序在输入正确的情况下必须要有正确输出,也就是程序的正确性一定要满足。
二.Why we need specification
1.我们用规约来保证程序的正确性。
在之前介绍软件的质量目标时有正确性这一指标,对开发人员来说,程序的正确性是一定要保证的。但是正确性要怎样来验证呢?是要把计算机中2^64个表示一一验证嘛?那是不可能的。
我们程序的正确性是一定要面向客户的。客户对程序的要求才是程序的标准。我们将这一标准限定在规约之中。于是我们用规约限定了程序完成什么样的功能,正确性的判断只需要判断是否满足规约中的功能说明即可。
2.规约是“供需双方”的契约,明确了“供需双方”需要遵守的规则。
①前面已经说明,程序的规约已经为开发人员制定了程序的标准。在开发人员与客户端达成一致后,将标准置于规约之中。由于已经协商一致,这样也避免了开发人员对功能有什么误解而带来未知bug
②同样地,规约也声明了客户端需要遵守的规则,在前置条件里声明。用户对程序的输入一定要满足其中的条件,如果不满足条件,那么责任属于客户端。
3.规约是“防火墙”,将用户所见与开发者具体实现隔离开来
①规约里只描述程序“能做什么”,而不描述“具体实现”。
②用户只需要理解规约,了解需要什么样的输入,完成什么样的功能,而无需阅读实现代码
③这样一来,将程序的具体实现与用户端隔离开来,开发人员可以灵活选择多种实现方式。
④满足同一规约的不同实现方法是“行为等价的”,因为在用户角度来看,它们完成了同样的功能
三.Details in specifications
1.除非在后置条件里声明过,否则方法内部不应该改变输入参数应尽量遵循此规则,尽量不设计mutating的spec,否则就容易引发bug
2.要尽量避免使用可变的对象,因为程序中可能有很多变量指向同一个可变对象(别名)。无法强迫类的实现体和客户端不保存可变变量的“别名”
3.规约与测试:黑盒测试在设计测试用例时就是严格按照规约来进行设计的。根据规约中的前置条件来进行输入域的等价类划分,再判断是否每一个输入都满足后置条件。黑盒测试不能与程序的实现方式有关。
四.Properties of specifications
1.规约的强度:如果一个规约的前置条件比另一个规约前置条件更弱,后置条件比另一个规约的后置条件更强,那么我们说这一个规约的强度大于另一个规约,并且可以用此规约来替代另一个规约。
注意:A的后置条件强于B是指一定要在满足B的前置条件前提之下,A的后置条件更强。
举例如下:
在图片中,后者的前置条件弱化了,但看后置条件强度的话,如果在满足前者的前置条件前提下,后者的后置条件同样夜若花了,因为返回的下标并不是最小的而是随机的。因此二者的强度是无法比较的。
越强的规约,意味着implementor的*度和责任越重,而client的责任越轻。
2.规约的确定性:
①确定的规约是指给定一个满足前置条件的输入,其输出是唯一的、明确的。
②非确定的规约:同一个输入,多次执行时得到的输出可能不同。比如随机数函数每次得到的随机数都可能不同。
③欠定的规约是指同一个输入可以有多个输出。但实现方式一旦确定以后,输出就是确定的,但是从规约中无法看出返回的是哪一个值
3.规约的陈述性:
①操作式规约,描述了程序具体实现的步骤。例如:伪代码
②声明式规约:没有内部实现的描述,只有“初-终”状态
③声明式规约更有价值,将实现细节放在代码实现部分的注释当中
五.How to design specifications
1.规约要设计为内聚的:规约描述的功能应单一、简单、易理解。尽量使每一个规约(方法)的功能都是整体功能当中最小的功能单元。一个规约(方法)实现了两个功能,那最好把它拆分为两个规约(方法)。比如一个方法既实现了将两个大写字符串转为小写的功能,又将转换后的小写字符串拼接。那么就可以拆分为两个方法来实现。
2.规约要保证其信息丰富(足够):可以说客户端理解程序的唯一窗口就是规约,因此规约中应当包含对用户端来说完备的信息,避免用户产生理解歧义
3.规约的强度适中:
太弱的规约,client不放心、不敢用 (因为没有给出足够的承诺)。开发者应尽可能考虑各种特殊情况,在后置条件中给出处理措施
太强的spec,在很多特殊情况下难以达到,给开发者增加了实现的难度(client当然非常高兴)。
4.推荐使用抽象类型,如接口:在规约里使用抽象类型,可以给方法的实现体与客户端更大的*度。比如使用接口,那么用户就可以使用多种类型的参数进行输入,而开发人员也有了更多的实现形式
5.权衡前置条件:
①如果不写前置条件,就要在代码内部check;有时可能代价过大
②客户端不喜欢太强的前置条件,不满足前置条件的输入会导致失败。
解决办法:不限定太强的前置条件,而是在后置条件中抛出异常:输入不合法
是否使用前置条件取决于(1) check的代价;(2) 方法的使用范围
– 如果只在类的内部使用该方法(private),那么可以不使用前置条件,在使用该方法的各个位置进行check——责任交给内部client;
– 如果在其他地方使用该方法(public),那么必须要使用前置条件,若client端不满足则方法抛出异常