OO第三单元博客作业

一、JML语言介绍

1.1、JML简介

  JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言,基于Larch方法构建。BISL提供了对 方法和类型的规格定义手段。所谓接口即一个方法或类型外部可见的内容。JML主要由Leavens教授在Larch上的工作,并融入了Betrand Meyer,John Guttag等人关于Design by Contract的研究成果。近年来,JML持续受到关注,为严格的程序设计提供了一套行之有效的方法。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMTSolver等工具以静态方式来检查代码实现对规格的满足情况。

1.2、JML部分基础语法

类别 语法名称 含义
原子表达式 \result 表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值
\old(expr) 用来表示一个表达式 expr 在相应方法执行前的取值
\not_assigned(x,y,...) 用来表示括号中的变量是否在方法执行过程中被赋值
\type(type) 返回类型type对应的类型(Class)
量化表达式 \forall 全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束
\exists 存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束
\sum 返回给定范围内的表达式的和
\max 返回给定范围内的表达式的最大值
方法规格描述 normal_behavior 正常行为规格的处理
expcetional_behavior 异常行为规格的处理
requires 用于描述前置条件,要求调用者确保P为真
ensures 用于描述后置条件,方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真
assignable 用于描述副作用范围,指出方法在执行过程中会修改对象的属性数据或者类的静态成员数据

1.3、JML工具链

  比较常见的JML相关工具有OpenJML以及JMLUnitNG。OpenJML可用于进行JML语法检查,以及依据JML的代码静态检查和动态检查;JMKUnitNG可以生成测试用例。下面提供工具下载地址(恬不知耻地把课程组提供的链接嫖过来)

OpenJML:http://www.eecs.ucf.edu/~leavens/JML//download.shtml

JMLUnitNG:http://insttech.secretninjaformalmethods.org/software/jmlunitng/

二、SMT Solver部署和检测

  利用OpenJML工具中的SMT Solver可以实现对JML语言进行检测。成功部署完OpenJML和SMT Solver之后可以进行JML语法检查、代码静态检查和动态检查。由于初次使用OpenJML不太熟悉,在对作业进行检查的时候出了很多问题,尝试了很多方法都无法得到解决,加上OpenJML不兼容forall和exist语法,因此再三考虑之后选择自己编写一个简单的JML进行验证。代码如下图所示。

OO第三单元博客作业_第1张图片

JML语法检查

  使用-check参数可以进行JML语法检查。在命令行中输入指令java -jar openjml.jar -check src\MainClass.java,即可进行JML语法检测,检测结果如下:

  由图可知,JML语法检测检测出了第13行的JML是有问题的,原因是12行末尾少了一个分号,将分号加上之后就能够再次通过检测。

静态检查

  使用-esc参数可以进行代码静态检查。不知道是什么原因Solver z3-4.3.2和z3-4.7.1都无法正常运行,因此在此处使用cvc4-1.6。在命令行中输入命令java -jar openjml.jar -esc -prover cvc4 -exec Solvers-windows\cvc4-1.6.exe src\MainClass.java,即可进行JML语法检测,检测结果如下:

OO第三单元博客作业_第2张图片

  由图可知,运行结果报出两个警告,通过静态检查发现了代码中存在除0和计算溢出的问题。

动态检查

  使用-rac参数可以进行代码动态检查。在命令行里一次输入命令java -jar openjml.jar -rac -exec Solvers-windows\cvc4-1.6.exe src\MainClass.javajava -cp jmlruntime.jar; src\MainClass.class,即可进行动态检查。

三、JMLUnitNG生成测试用例

  此次还是对于上述写的测试代码进行测试用例的生成。在命令行中输入命令java -jar jmlunitng-1_4.jar src\MainClass.java,就会在src文件夹下生成一堆测试用的.java文件。

OO第三单元博客作业_第3张图片

  其中有一个MainClass_JML_Test.java文件是可以直接运行的,将其运行即可对生成的样例进行测试。

OO第三单元博客作业_第4张图片

  从中我们可以发现,生成的测试用例基本上都是边界数据和异常数据,覆盖程度非常小,因此如果想要保证程序的正确性,还是需要写对拍器进行辅助测试。

四、作业架构分析

  本单元旨在构建一个社交关系的网络,并通过网络的一些特点获取其中的信息。本次单元着眼于规格设计,学习了契约式设计的JML语言。yysy,本单元在代码实现上难度并不是特别高,除了第三次作业的queryMinPath和queryStrongLink这两个方法需要用到图论的知识以及图路径的遍历,需要仔细思考算法之外,重点是放在理解JML语言上面,需要我们全方面理解这个方法的通途,以及各种输入的情况;另外在这个单元里性能也是极其重要的,不能因为实现简单而掉以轻心,导致作业强测翻车。

第一次作业

UML图

OO第三单元博客作业_第5张图片

  第一次作业没什么太大的难度,只要按照每个方法的JML规格实现Network类和Person类就可以了。因为是第一次作业,对于JML语言还不是特别了解,以为我们实现的代码必须和JML里定义的数据一样才可以,因此一开始用的全都是静态数组,后来看到群里说可以使用Arraylist等容器,然后查阅相关资料弄清楚了JML语言描述的只是一个规范和限制,具体的代码实现只要保证符合JML规范就可以了,不需要一定按照JML定义的数据类型实现,因此后来people、value等都采用Arraylist存储。

  因为简单,所以就没有对JML规格实现进行更为深入的思考,更没有意识到不注重优化性能会带来多么严重的后果,而且唯一一个需要一定算法的iscircle方法也因为第一个想到的BFS就没有分析比较其他算法的优缺点,为第二次作业翻车埋下伏笔。

bug分析

  此次作业在强测和互测中都没有被发现bug,我尝试交了几个边缘数据也没有成功hack到。在本地测试中,我采用的方法是手动编写边缘数据+与同学对拍的模式进行测试,最终还是测出来一个bug,这个bug是addRelation方法里,我将判断两个人是否为同一个人放在了判断两个人是否isLinked,这样就会导致当两个id相同时会抛出异常EqualRelationException的异常,而不是直接return,这是没有好好注意判断顺序的原因。

第二次作业

UML图

OO第三单元博客作业_第6张图片

  第二次作业在第一次作业的基础上增加了Group类,Group的方法中也没有比较吃算法的方法,根据JML规格写就行了;而Network里增加了和Group相关的方法,整理上来说并没有什么太大的变化,其他方法基本上就沿用第一次作业,没有进行任何优化。因为没有优化,导致我的强测原地爆炸。

bug分析

  在前面也多次提到过我第二次作业的强测直接爆炸,20个点只通过了3个点,最后也没进互测。等到强测结果出来之后我仔细研究了一下,全都是CTLE,也就是说我的代码正确性是没有问题的,但是性能极差。通过对代码全面分析比较之后,导致我CTLE的原因有两个大方面:其一是NetWrok类中所有需要通过id得到person的方法,我都是先用contains方法判断这个person是否在Network中,然后每次需要使用到person的时候都直接调用getPerson(id)方法,这样的话就导致增加了许多次无用的遍历people,其实只需要一开始遍历一遍缓存下来就可以了,完全可以避免多次遍历;这还不是最主要的问题,我queryNameRank函数对比较的判断直接调用compareName函数,理论上是完全没有问题的,但是仔细分析,会发现如果直接调用comapreName的话,会多出 n^2 对people的遍历,使得queryNameRank函数复杂度由O(n)变为O(n^2),复杂度急剧提升。当我修改完这个问题之后,一次性过了16个点。

  第二个方面是对Group内的数据管理问题。Group内的很多方法都是O(n^2)的遍历问题,比如getRelationSum等一众方法,如果每次调用函数的话都要进行一次双重遍历,那么程序就会很慢,因此在Group中缓存sum是一个很好的解决办法。分析可以得知,只有Network的addRelation方法和Group的addPerson方法会导致sum发生变化,因此只要在这两种方法中增加对sum的更新,每次调用函数直接返回sum就可以了,复杂度为O(1)。

  此次本地测试采用的依旧是边缘数据测试加和同学对拍,实际上这样的测试只能够保证代码的正确程度,并不能够保证效率在允许的范围内,因此,在以后的测试中还需要读取CPU时间进行评判。

  得知我没有进互测之后一整天都没有过好,等强测结果出来后进行了深刻的反思。这次翻车首先是因为自己的大意,认为最难的多线程单元已经过去了,后面的单元没那么难了;加上之前也没太出现过超时的问题,也就没太在意性能方面的问题;再有就是从第一单元养成的直接翻译JML的坏习惯,没有做好分析架构。在第三次作业中,我吸取了这次作业的教训,认真分析架构与实现,还是取得了较好的效果。

第三次作业

UML图

OO第三单元博客作业_第7张图片

  第三次作业主要在Network类中新增了部分方法,其中最为重要而且难以实现的是queryMinPath和queryStrongLinked这两个方法。上述的两种方法均是对图的查询,一个是对最短路径查询的操作,另一个是对点双连通的判断操作,因此需要我们用到一定的算法来解决问题。

  对于queryMinPath求最短路径算法来说,我尝试过Floyd算法和Dijkstra算法,最后还是选择用堆优化的Dijkstra算法。经过测试,Floyd算法虽然可能缓存结果,但是由于其 O(n^3) 的算法复杂度,导致其在n特别大的时候计算非常慢,不适合在不限定addRelation条数的情况下使用。Dijkstra算法本身是O(n^2)复杂度的算法,在经过对遍历最短节点进行堆优化之后,其复杂度降至O(n*logm),对于本次作业来说完全够用。

  queryStrongLink方法是本次作业最难的一个点,涉及到点双连通分量的处理。方法有很多,可以暴力DFS,可以删点,也可以采用高级算法Tarjan,最后我还是选择用Tarjan算法实现,一是因为其他方法比如暴力求解可能会存在一定的问题,不太稳妥,再有就是Tarjan虽然比较难理解,但是效率绝对是很高的,绝对不会超时加上第二次作业的影响,最后选择了Tarjan算法实现。

bug分析

  第三次作业由于有第二次作业的前车之鉴,加上对关键方法使用了较为高效的算法,我在强测和互测中都没有被找到bug。本地测试采用的方法依旧和以前一样,也测出来几个问题。一个是group类的delete方法,我只是简单地将一个person从group中移除,没有考虑到对缓存的sum等数据的影响,对此需要和addperson方法一样及时更新数据;另外就是我写的第一版Tarjan是有问题的,一开始我的堆栈里面存的是遍历到的点,由于此次的社交图是无向图,不能直接拿一般的Tarjan算法套,这样的话会导致一个点在两个不连通的环上时会判断错误,因此我新增了边Edge类,堆栈中存的是遍历过的边,这样能够保证遍历边的唯一性。

五、总结与反思

  通过本单元三次作业的学习,我基本掌握了JML语言,深入了解了这种契约化设计,能够但还不能熟练运用OpenJML、JMLUnitNG和JUnit进行形式化验证。在今后的学习和代码设计过程中,一定要学会将JML语言运用到实际中去,不光光是学会阅读JML,更要学会写JML,充分利用JML语言带来的便利。

  这次单元作业我认为是前三单元中最简单的一次作业,但实际上确是我得分最低的一次,这一切归咎于我的大意。不管以后的学习有多简单,都要抱着严谨的态度,踏实学习,方能取得成功。

你可能感兴趣的:(OO第三单元博客作业)