OO第三单元总结-JML

OO第三单元总结-JML

一、 JML语言总结

1. 理论基础

JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言,提供了对方法和类型的规格定义手段。JML为严格的程序设计提供了一套行之有效的方法。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。

一般而言,JML有两种主要的用法:

(1)开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。

(2)针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。

JML的语法分为几个层次,level 0是JML最核心的语言特征,下面仅针对level 0语言特征进行小结。

1.1 JML表达式

常用表达式

\result :表示一个非void类型的方法执行所获得的结果,即方法执行后的返回值。

\old(expr) :用来表示一个表达式expr在相应方法执行前的取值。注意\old(group.length)代表的是group在执行方法前的长度。而\old(group).length == group.length,是方法执行后的长度。这是由于在方法执行过程中,并没有改变group指向的对象,只是改变了其指向对象的值。group是一个引用,指向的地址是不变的,也就是 \old(group) = group。

\not_assigned(x,y,...) :用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为true,否则返回false

\not_modified(x,y,...) :与上面的\not_assigned表达式类似,该表达式限制括号中的变量在方法执行期间的取值未发生变化。

\forall :全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。

\exists :存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。

\sum :返回给定范围内的表达式的和。

\product :返回给定范围内的表达式的连乘结果。

\max :返回给定范围内的表达式的最大值。

\min :返回给定范围内的表达式的最小值。

\num_of :返回指定变量中满足相应条件的取值个数。

等价关系操作符b_expr1<==>b_expr2或者b_expr1<=!=>b_expr2

推理操作符b_expr1==>b_expr2或者b_expr2<==b_expr1

\nothing :指示一个空集。

\everything :指示一个全集。

1.2 方法规格

方法规格的核心内容包括三个方面,前置条件、后置条件和副作用约定。从规格的角度,JML区分这两种场景,分别对应正常行为规格(normal_behavior)异常行为规格(expcetional_behavior)

前置条件(require):对方法输入参数的限制,如果不满足前置条件,方法执行结果不可预测,或者说不保证方法执行结果的正确性。

后置条件(ensure):对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,否则执行错误。

副作用(assignable):规定方法在执行过程中对输入对象或this对象的什么成员进行了修改(对其成员变量进行了赋值,或者调用其修改方法)。

/*@ pure @*/ :设计中会出现某些纯粹访问性的方法,即不会对对象的状态进行任何改变,也不需要提供输入参数,这样的方法无需描述前置条件,也不会有任何副作用,且执行一定会正常结束。对于这类方法,可以使用简单的(轻量级)方式来描述其规格,即使用pure关键词。

异常(signals, signals_only):对于异常行为规格,通常会使用signals或signals_only子句来抛出异常。signals子句的结构为signals (Exception e) expr;,当expr 为true 时,方法会抛出相应异常e;signals_only子句的结构为signals_only Exception;,意思是一旦进入此子句,就会抛出相应的异常。

1.3 类型规格

类型规格指针对Java程序中定义的数据类型所设计的限制规则,一般而言,就是指针对类或接口所设计的约束规则。课程主要涉及两类,不变式限制(invariant)约束限制(constraints)。一旦类型中定义的数据成员违反限制规则,就称相应的状态有错。

不变式invariant :要求在所有可见状态下都必须满足的特性。

状态变化约束constraint :对状态的变化进行约束,即对前序可见状态和当前可见状态的关系进行约束。

2. 应用工具链情况

JML的工具链较不完善,不成熟,使用起来困难。常用的JML工具主要有:

OpenJML:对JML进行语法检查、配合Solver进行简单的静态验证、以及运行时验证。

JMLUnitNG/JMLUnit:自动化单元测试生成工具,根据规格自动生成测试。

Junit:单元测试,根据规格自行手动构造测试。可以检查覆盖率,应尽量保证覆盖率高(但不是全部覆盖就表示没有bug)。

由于OpenJML和JMLUnitNG功能有限且使用困难,所以在本单元的作业过程中我主要还是采用手动构造单元测试的方法。

二、 部署JMLUnitNG

如下图所示,部署了JMLUnitNG,针对Group接口的实现自动生成测试用例,并进行了测试。

OO第三单元总结-JML_第1张图片

OO第三单元总结-JML_第2张图片

从图中可以看出,自动生成的测试主要是针对了边界数据和null进行了简单的测试。在上面的测试中,failed的测试点主要是由于方法传入null而失败,由于无法生成Person,所以总是将null传入了addPerson这类方法中。而本单元作业中,许多方法不会传入null,因此在代码中未进行处理,个人猜测就是失败的原因。

由此可以看出,采用JMLUnitNG进行自动测试其实有很大的局限性,并且本人在实际操作过程中也感觉非常麻烦,不如自己手动构造单元测试用例。

三、 作业架构设计

1. 第一次作业

1.1 架构设计

根据已经给出的基础架构和Network、Person两个类,按照JML实现MyNetwork和MyPerson。由于刚刚接触JML,没有多想,就直接把JML中的数组操作翻译过来用了ArrayList。

OO第三单元总结-JML_第3张图片

1.2 算法分析

把JML的数组全部直接实现成了ArrayList,JML中的循环遍历也对照着直接循环遍历。

第一次作业稍微复杂的函数只有一个isCircle,就是判断两个点是否连通,直接用一个bfs或dfs就能解决。

因为第一次作业涉及的操作都比较简单,稍微复杂的isCircle直接用一个bfs或dfs也能解决,所以性能要求很低。这种对照JML无脑循环的方式也可以没有任何bug的通过中测,拿到强测满分。但如果第二次也这样做而不考虑性能,就直接GG。

1.3 复杂度分析

​ Myperson复杂度

OO第三单元总结-JML_第4张图片

​ Mynetwork复杂度

OO第三单元总结-JML_第5张图片

因为功能简单,总的来说复杂度比较低。

1.4 bug分析

第一次作业没有出现bug,互测没有被hack,强测也拿到了满分。但这种不考虑性能的架构,一旦功能变得更加复杂,存在很大被卡的风险。

2. 第二次作业

2.1 架构设计

第二次作业多了Group层次,介于Network和Person之间,总体架构是课程组规定好的,不需要大幅度更改。由于第二次作业测试的指令条数多达10w条,对性能要求比较高,所以第一次那种直接按照JML翻译为ArrayList的做法必然超时。

考虑到查询操作比较多,故引入HashMap,需要遍历的操作仍使用ArrayList,使用两种容器同时存储的方法,以空间换时间,提高查询操作的性能。判断存在或者取出某个对象时,利用hashmap,以id为索引。遍历时利用arraylist,可以按顺序遍历,并且更加简单直接。

OO第三单元总结-JML_第6张图片

2.2 算法分析

关于算法,这次作业有两个坑点。

第一个是isCircle方法中不能使用dfs,只能使用bfs。因为测试指令数可以多达10w条,在互测中,当构造一个很长的单链的时候,使用dfs查询单链两头的结点就会导致递归层数过多而爆栈。而bfs使用的是自己构造的队列,所以不会有问题。这个问题我注意到了,所以并没有被hack。

第二个是queryGroupRelationSumqueryGroupValueSum操作,对应到Group中是getRelationSumgetValueSum这两个方法。我虽然使用了HashMap来减小了查询操作的复杂度,但是在这两个方法中仍然使用的是按照规格的双循环遍历,方法的复杂度为o(n^2),在10w条指令的轰炸之下,出现了超时。

观察所有算法里面,只有这两个函数是o(n^2)的,对于这两个方法,正确的算法是采用缓存机制。在MyGroup中增加了relationSumvalueSum两个属性,在每次将人加入group的时候更新,在每次加关系的时候也更新。

先是加人的时候和组里面所有人比对,如果有link的人,就更新relationsum和valuesum,除此之外,还要加上relationsum的自圈数。

当两个人已经在组内,添加他们关系的时候,采用牺牲封闭性的方法,在MyGroup增加了updateRelation的public方法,允许外界更新relationsum,valuesum。加关系的时候遍历network中的组,判断两个人都在组里面的话,就更新这个组的有关数据。

2.3 复杂度分析

​ MyPerson复杂度:由于引入hashmap,查询复杂度降低。

OO第三单元总结-JML_第7张图片

​ MyGroup复杂度:由于引入hashmap,再加上采用了缓存机制,复杂度很低。

OO第三单元总结-JML_第8张图片

​ MyNetwork复杂度: 主要复杂度在于添加关系时还有向组里加人时,这是因为对于缓存的维护。

OO第三单元总结-JML_第9张图片

2.4 bug分析

就是由于上面已经说过的getRelationSumgetValueSum两个方法我采用了双循环,所以强测有一个点超时,互测也被hack了不少。修改也如上所述,采用缓存机制。

3. 第三次作业

3.1 架构设计

第三次作业引入了一个不怎么重要的属性money,这很好解决,使用了hashmap,索引为人的id,money为值。还增加了delFromGroup方法,只要注意到对缓存的维护即可。而这次最大的难点是在于新增的三个非常复杂的方法:queryMinPathqueryStrongLinkedqueryBlockSum。这次虽然测试指令条数减少了,但对于算法的要求大大提高了,对性能仍有很高的要求。

OO第三单元总结-JML_第10张图片

3.2 算法分析

这次算法主要有三大难点:

第一,queryMinPath,求两点间最短路径。

我采用的是Dijkstra算法,但是由于没有进行堆优化,导致强测超时。堆优化的主要思想就是使用一个优先队列(就是每次弹出的元素一定是整个队列中最小的元素)来代替最近距离的查找,这样可以减少一个查找的循环,大幅度节约时间开销。定义一个类Node,它包含一个节点的编号以及该节点当前与起点的距离。而PriorityQueue queue就是使用到的优先队列。

第二,queryStrongLinked,判断两点是否点双连通。

我一开始采用的方法是求出两点间的所有连通路径,再比较是否存在两条点不相同的路径。显然,这样的算法在结点和关系很多时,由于路径条数急剧增多,复杂度也非常大。在强测中,毫无疑问地超时了。

课程组给出的算法是这样的:如果存在一个点,把它从图中删除后使得询问的两个点不连通,那么答案为false,否则为true。在实现时暴力枚举所有点,把它从图中删除然后遍历即可,都是很基础的图操作,标程也是使用的这种做法。

这种算法复杂度接近o(n^2),在这样的指令条数和时间限制下是可以接受的。

第三,queryBlockSum,求连通块数。

求连通块数最好的方法应该是并查集。但是由于构建起来比较费事,我没有为了使用并查集而额外更改架构,而是使用了bfs查连通块数的方法,因为bfs每次调用都会遍历一个连通块,所以只要记录bfs调用的次数就能知道连通块数,这样的方法也同样能满足性能要求。

3.3 复杂度分析

​ MyPerson复杂度:没有太大变化,就不列出了。

​ MyGroup复杂度:只是增加了delPerson方法,复杂度也没有太大变化。

​ MyNetwork复杂度:主要复杂度都在新增的三个比较难的方法上,尤其是queryStrongLinkedqueryMinPath

OO第三单元总结-JML_第11张图片 OO第三单元总结-JML_第12张图片

3.4 bug分析

如上所述,这次bug都是因为算法超时,强测有6个点超时,都是因为queryMinPathqueryStrongLinked的算法问题。queryMinPath的Dijkstra算法没有进行堆优化,而queryStrongLinked选择了错误的算法。经过算法的优化和修改,已经通过了bug修复。

四、 心得体会

1. 对于JML的理解

相比单元刚开始时完全没有接触过JML,再到经历了三次作业训练后能比较好地读懂JML并且加以实现,我对于JML语言的理解也有了很大的提高。JML将原本模糊性的自然语言描述变成了逻辑严格的规格,可以消除开发过程中的歧义性,还可以方便进行覆盖测试,实在是开发过程中不可缺少的工具。

多次作业下来,我对JML规格有了更加深刻的认识,虽然因为自身算法功底的不足,一些算法没能成功实现,出现超时。但是我深刻感受到了规格在写代码时的作用。规格只关心允许你改什么数据,改完数据后要满足什么条件,但不关心你取得结果的方法,不规定具体的实现方法。对于规格的理解,我收获许多。

2. 对于Junit单元测试的运用

在三次作业过程中,我每次都写了Junit测试用例,对自己的实现进行了单元测试。在写测试用例时,我是对照着JML规格写的,尽量做到了高覆盖率。这种精细到方法级甚至语句级的测试,对于判断自己的方法实现是否正确是非常有用的。但是,我运用Junit只能做到功能是否正确的测试,不能测试指令非常多的时候的超时问题,这点让人十分头疼。

3. 对于java语言的掌握

对于不同容器的选择,ArrayList和HashMap各自的优势,这次我是真切地感受到了。

对于算法的把握,对时间复杂度的分析,是很重要的收获。并且让我意识到了自己的算法功底还远远不足。

你可能感兴趣的:(OO第三单元总结-JML)