OO第三单元——基于JML的社交网络总结
一、JML知识梳理
1)JML的语言基础以及基本语法
JML是用于java程序进行规格化设计的一种表示语言,是一种行为接口规格语言。其为严格的程序设计提供了一套行之有效的方法。通过JML以及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格满足情况。一般来说,其运用于以下两种情况:①开展规格化设计②针对已有的代码,书写其对应的规格,从而提高代码的可维护性。
基本语法:
关键词 |
含义 |
pure |
纯粹的查询方法 |
non_null |
非null修饰 |
normal_behavior |
正常行为界定 |
exceptional_behavior |
异常行为界定 |
requires |
前置条件,满足才可进入 |
ensures |
结束行为的后置条件,保证要求达到 |
assignable |
副作用,可赋值 |
modifiable |
副作用,可修改 |
signals(e) |
异常行为时,抛出异常e |
signal_only |
满足前置条件抛出异常 |
invariant |
不变式,在所有可见状态下,都要满足的性质 |
constraint |
状态变化的约束 |
esult |
方法执行后的返回值 |
old(x) |
x在方法执行前的值 |
ot_assigned(x, y, ...) |
括号内的元素全程不对其赋值 |
ot_modified(x, y, ...) |
括号内元素在函数执行时不该改变值 |
(forall T x; R(x); P(x)) |
全程量词,满足R(x)的都可以满足P(x) |
(exists T x; R(x); P(x)) |
存在量词,存在一个满足R(x)同时也满足P(x) |
(sum T x; R(x); |
对满足R(x)的x,其对应的表达式求和 |
(max T x; R(x); |
对满足R(x)的x,其对应表达式的极大值 |
(min T x; R(x); |
类上,求极小值 |
&& 、||、==、==>、<==> |
与、或、相等、蕴含、互蕴含 |
应用工具链:
JML可以使用的工具链如下:
·(普普通通)Junit:简单的进行构造数据,对其进行规格化检验。
junit.jar https://repo1.maven.org/maven2/junit/junit/4.13/
hamcrest-core.jar https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/
两个下载地址,可以下载jar包,然后再IDEA进行添加包就可以测试了。
·OpenJML:可以实现JML语法错误检查。
可以在官网进行下载。http://www.openjml.org/downloads/ ,解压到本地然后利用cmd,即可
java -jar .openjml.jar -exec 解释器绝对路径 -esc 被检测的JAVA文件的绝对路径(如果为文件夹或包,记得前面加 -dir)
·SMTSolver: 在逻辑层面、对代码实现形式化验证。
·JMLUnitNG: 根据JML规格生成对应的测试样例来测试程序。
可以在http://insttech.secretninjaformalmethods.org/software/jmlunitng/下载jar包,然后利用cmd进行使用,具体操作见下一章。
二、openJML以及SMT Solver部署
本地使用的MT Solver为z3版。
1)测试已有的作业代码
首先本地运行openJML时发现其不支持三目运算符,所以将存在的
esult == (people.length == 0? 0 : ...
修改为:
/*@ public normal_behavior @ requires people.length == 0; @ ensures esult == 0; @ also @ public normal_behavior @ requires people.length > 0; @ ensures esult == ((sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length); @*/
(另一块类似操作)
之后运行后本地结果为:
其中大部分警告是The prover cannot establish an assertion (ArithmeticOperationRange),这是因为我们给出的规格上并没有明确的JML规格要求,所以这里无法给出断言。
两个独特的是:Precondition conjunct is false,是原因规格并没有对person进行非null的限定(虽然运用equal是可以的,但是规格中使用的是==,所以严格来讲要对输入的person进行非null限定)。
Associated declaration:是因为自己在实现Mygoup的时候使用了这个函数(至于为什么造成这个警告我也很迷)。
2)测试一个简单的代码程序
public class Testclass { public static void main(String[] args) { Testclass testclass = new Testclass(); System.out.println(testclass.mul(520, 521)); System.out.println(testclass.div(10, 5)); System.out.println(testclass.mod(10, 3)); } //@ensures esult == a * b; public int mul(int a, int b) { return a * b; } //@ensures esult == a / b; public int div(int a, int b) { return a / b; } //@ensures esult == a % b; public int mod(int a, int b) { return a % b; } }
(这是一个很多人都使用的代码,其中div和mod有明显的错误(不能对0进行操作))
测试结果:
我们注意到2、3条警告信息,PossiblyDivideByZero明确的指出了,div和mod操作不能对0进行运算。而另外两个是出于算数溢出的考虑(ArithmeticOperationRange)。
三、JMLunitng测试
下载JMLunit的jar包:http://insttech.secretninjaformalmethods.org/software/jmlunitng/
点击1.4即可下载。
1、然后,将所下载的jar包添加到我们的project中(具体内容可以参考junit实验时候的添加jar包操作)
2、在相应的路径下(本人选择src目录下)用git bash(cmd也可),生成自动测试用例指令如下:
java -jar openjml/jmlunitng.jar(jmlnnit.jar的路径) com/oocourse/spec3/main/MyGroup.java(Mygroup路径) com/oocourse/spec3/*(其余文件路径)
(以上都是在src下的相对路径)
3、运行时要修改一些代码(自己的问题和官方代码书写的不规范):
出现52、51的警告信息可以忽略(这是版本不匹配)。然后对于所存在的new Arraylist<>()等等(Hashmap/Hashset)中<>的内容都要补全,官方代码出现的0<=i<xxx.length不符合要求需要改成0<=i&&i<xxx.length。
4、成功生成代码之后,我们只需要运行MyGroup_JML_Test.java这一个已经生成的测试文件
本人结果如下:
[TestNG] Running: Command line suite Failed: racEnabled() Passed: constructor MyGroup(-2147483648) Passed: constructor MyGroup(0) Passed: constructor MyGroup(2147483647) Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.addPerson(null) Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.addPerson(null) Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.addPerson(null) Failed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.delPerson(null) Failed: <<com.oocourse.spec3.main.MyGroup@1f>>.delPerson(null) Failed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.delPerson(null) Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.equals(null) Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.equals(null) Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.equals(null) Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.equals(java.lang.Object@5479e3f) Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.equals(java.lang.Object@27082746) Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.equals(java.lang.Object@66133adc) Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.getAgeMean() Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.getAgeMean() Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.getAgeMean() Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.getAgeVar() Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.getAgeVar() Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.getAgeVar() Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.getConflictSum() Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.getConflictSum() Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.getConflictSum() Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.getId() Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.getId() Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.getId() Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.getPeople() Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.getPeople() Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.getPeople() Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.getRelationS() Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.getRelationS() Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.getRelationS() Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.getRelationSum() Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.getRelationSum() Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.getRelationSum() Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.getValueS() Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.getValueS() Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.getValueS() Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.getValueSum() Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.getValueSum() Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.getValueSum() Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.hasPerson(null) Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.hasPerson(null) Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.hasPerson(null) Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.hashCode() Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.hashCode() Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.hashCode() Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.setRelationS(-2147483648) Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.setRelationS(-2147483648) Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.setRelationS(-2147483648) Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.setRelationS(0) Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.setRelationS(0) Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.setRelationS(0) Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.setRelationS(2147483647) Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.setRelationS(2147483647) Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.setRelationS(2147483647) Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.setValueS(-2147483648) Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.setValueS(-2147483648) Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.setValueS(-2147483648) Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.setValueS(0) Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.setValueS(0) Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.setValueS(0) Passed: <<com.oocourse.spec3.main.MyGroup@8000001f>>.setValueS(2147483647) Passed: <<com.oocourse.spec3.main.MyGroup@1f>>.setValueS(2147483647) Passed: <<com.oocourse.spec3.main.MyGroup@8000001e>>.setValueS(2147483647) =============================================== Command line suite Total tests run: 67, Failures: 4, Skips: 0 =============================================== Process finished with exit code 0
通过运行结果发现,jmlunitng生成的测试用例主要是对边界情况的测试用例。我们可以看到许多的如2147483647和-2147483648和0等测试用例,对于引用类的测试都是用null来测试的。这样子的测试数据比较特殊,缺少随机性,所以对于正确性检验,强度不是很大。(比如本次出现的3个没有通过的点,都是对对象没有null的判断,但是在本次作业中,我们的输出是不会出现null的,所以正确性还是有保证的,但是这么多pass我就很安全了吗?)
四、设计架构
1)总论
三次作业的设计架构都是基于JML规格进行代码实现的,整体上架构思路还是比较清晰的,最后完成的是一个社交网络Network。其中,社交网络包括三个类——Person,Group,Network。Person类作为基本元素,拥有如ID、姓名、性格、认识的人群的基本信息,Group表示在整个网络中存在的小的组,Network是整个社交网络,内含有多种操作。三次作业完成的架构图如下:
(基于JML所以架构还是非常清晰的)
三次作业不难看出,算法上的考察点基本上是图论的问题,所以数据结构学习很重要(悔数据结构摸鱼)
2)分论
第一次作业
第一次作业基本上按着JML的意思,就可以知道大概的意思,所以第一次作业没有很大的难点,主要的难点就是iscircle函数(函数名太迷惑了,一开始jml没认真看,就以为是寻找环,差点就要提前学习Tarjan了)。
iscircle的函数主要使用了bfs的思想,从id1开始广度搜索,如果无法连接到id2,则返回false。
第二次作业
第二次作业,总体上难度上升比较平和,主要是加强了数据量,在第二次作业更多的要考虑实现的复杂度。第二次作业架构上加入了Group的类,主要实现一个群里的相关数据的总和,这个时候就需要遍历每一个成员,这个时候就要谨慎复杂度,如valuesum若用双循环来写的话,每一次查询都很麻烦。所以在Group类中valuesum,relationsum等成员变量优化了算法,只需在Group添加成员的时候,和添加关系的时候维护一下变量即可。
另一方面,第二次作业相对于第一次,把第一次数组结构进行了优化,(Arraylist转为Hashmap,由id直接得到Person变量,一定程度上优化了)
第三次作业
(个人感觉,突然就不温水煮青蛙了,直接打算卸磨杀驴了)第三次作业,难度上升较大。虽然增加新的方法不多(增加的借钱系统,个人感觉只是为了丰富这个社交网络的实现,难度实现并不大),本次作业个人觉得难度大的函数有二——qmp寻找最小路径(Dijstra算法)、qsl寻找点联通分量(Tarjan算法)。两个函数难度体现在算法上,具体算法可以参考CSDN上多篇博客,算法优化放在后面的bug分析上。
然后在实现querryBlockSum,一下子傻眼了了,如果按照前面两次作业的思维,根据jml写代码,那么直接用jml所说的双循环,再判断是否iscircle,那这样的复杂度绝对很大。最后在大佬的帮助下,渐渐意识到,这个函数的jml的可以理解成有多少连通图,即一旦两个节点连通,那么这两个点可以合成一个图,再来一个点和二者之中一个连通,又成了一个连通图(开始意识到并查集的使用),那么一旦两个不连通,就分别存在两个图中。
所以第三次作业实现上,大改前面的风格,使用了并查集的方法。这一方法使用,大规模降低了iscircle的复杂程度,不过对于querryBlockSum的函数实现,还是使用了维护的方法,加入一个人,blocksum+1,添加关系时,先判断是否iscircle(此时的复制度就o(1)),如果不是则blocksum-1。其他方面,由于并查集优化了iscircle函数,所以在上述最小路径和寻找连通分量的算法,可以先运用iscircle进行快速判断,排除错误情况(我觉得第三次作业还是老老实实用我之前的做法来写的话,十之八九会当场死于cputle)
3)再次总结
架构上,还是没有做到迭代开发,第三次相对于前两次而言改动太大了。同时,也是印证了助教们的提示,不要照着jml写代码!!!看懂jml在说什么不难,但是讲说的内容和题目背景,以及图论一些架构合在一起理解就需要自己的思考,比如qsl,将存在两条不重合的路径理解成存在点连通分量并想到使用Tarjan算法来实现就很有难度(如果按jml意思暴力求解,可行性极低,并将tle)。所以架构上的思考还是需要有的。
五、BUG分析和修改策略
第一次作业
强测互测安然无事,主要还是第一次作业难度不大,主要卡的点也是iscircle函数,在与大佬多次对拍之后,亦可完善,并且,第一次作业对复杂度要求不高,所以iscircle实现起来也不用考虑其复杂度。
第二次作业
强测被炸四个点,而且这个bug出现的也是独特(在众多more than之中就我一个less than),主要原因在于判断的顺序,在addToGroup函数中中jml规定了group中人数不超过1111,大部分人选择不判,而我选择了先判定,这样就造成了,如果一个group人数超过1111,并且加入的这个人是非法的,不会抛出异常,而是会直接先返回,所以输出少了,所以给出的建议就是,先判异常再判断其他。
互测被某狠心玩家测爆了iscircle的复杂度。理论上第二次即使数据再大,也不会测爆一般规格的bfs的算法。但是在构建队列的时候,标记人员是否使用过的时候,使用了indexof函数(事后才知道这是一个o(n)复杂度的函数),所以使得复杂度被强硬的提升了一级。修改上,就是利用其他更高效的容器,hashset存储标记过的人员,使用contais函数进行判断是否标记,优化了函数。
第三次作业
强测依旧死于tle。主要是最短路径的求法上出了问题,不加以优化的Dijistra算法复杂度较高,所以修改时,学习了优先队列的相关知识,使用优先队列,省去了每次寻找最小值的复杂。大大降低了复杂度。
互测自己并未发现bug,但是依靠自己对代码的理解,在互测中hack了7刀。首先,是对qsl的jml代码理解,虽然我们可以理解成双连通分量,但是对于有且只有两个互相关联,它们是双连分量(用Tarjan算法求得),但是在本题的jml规格中并不是,所以可以hack一把。
其次是针对qsl和最小路径复杂度攻击,qsl实现如果用双bfs会存在重大bug。其次对于以下图:
暴力求解会存在效率问题。所以自己和小伙伴一起捏造了该数据进行hack(我的屋中,这一刀hack了4个人)
六、总结
相比与前面两个单元,jml单元对测试的能力要求更高,无论是基于Juint的基础测试,还是自己构造的数据测试,都要求自己能看懂JML规格,根据所理解的规格构造对应的针对数据,对边缘条件进行深度测试。另外一方面,JML这一种规格描述语言,给我最大的体会就是,面向对象的重点不单单是在实现,也在于整体架构,JML就是强调了架构,弱化了具体过程的实现,换句话说,剥离了二者过程,架构师架构了思维,想让程序员实现方法,并不是通过神奇的”先这样再那样“的量子对话,而是用JML实现了二者交流,完成了一个具体项目的实现。
再回到本单元作业实现的本身上,在代码实现上的体会在上文也说到了。但是再具体到其中某些算法的实现,不难看出,本单元更多的与图进行对接,而自己在代码实现中,对图论的知识了解甚少,尤其是到了最后一次,出于无奈之下,才被迫研究起了并查集。而且对于已经学过的迪杰斯特拉算法和未学过的Tarjan算法,在理解上都很难做到吃透吃全。所以下一单元大概率还是基于图来学习新的知识,自己在图论方面的能力也有待提高。
最后,希望自己能够再接再厉,最后在征战第四单元能有更多收获。
OO第三单元——基于JML的社交网络总结