OO第三单元——基于JML的社交网络总结

 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);exp)

对满足R(x)的x,其对应的表达式求和

(max T x; R(x);exp)

对满足R(x)的x,其对应表达式的极大值

(min T x; R(x);exp)

类上,求极小值

&& 、||、==、==>、<==>

与、或、相等、蕴含、互蕴含

应用工具链:

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的社交网络总结

原文地址:https://www.cnblogs.com/cwm18373197/p/12926781.html