一、JML理论基础及相关工具链
1.JML理论基础
该部分梳理本单元作业中涉及到的JML知识。
1.1注释结构
JML采用javadoc注释的方式来表示规格,且每行以@开头。通过使用//@annotation来进行行注释,使用/*@annotaion@*/来进行块注释。
1.2JML表达式
1.2.1原子表达式
-
\result表达式,在方法规格中使用,通过\result来指代返回值。在谓词中使用\result,来表达放回值的限制条件。
- \old(expr)表达式,返回表达式expr在方法执行之前的值。
- \not_assigned(x,y,...)表达式,该表达式为一谓词,当括号中变量在方法执行过程中未被赋值返回true,否则返回false。
- \not_modified(x,y,...)表达式,该表示为一谓词,当括号中变量在方法执行过程取值为变化返回true,否则返回false。
1.2.2量化表达式
- \forall表达式。表达式格式为\formall V; P1; P2。其中V定义了在该表达式中要使用的变量,谓词P1、P2为变量需要满足的条件。一般P1表示变量的范围,P2表示变量需要满足的其他条件。该表达式整体也为一个谓词,当V定义的变量的所有满足P1的取值都满足P2时返回true,否则返回false。
- \exists表达式。表达式格式为\exists V; P1; P2。其中V,P1,P2与\forall表达式中的相同。该表达式整体为一个谓词,当V定义的变量存在一个满足P1的取值也满足P2时返回true,否则返回false。
- \sum表达式。表达式格式为\sum V; P; N。其中V定义了表达式中要使用的变量,谓词P限制了变量的范围,N表示每一个满足P的情况下的增量。表达式最终返回所有增量的加和。
1.2.3操作符
- 推理操作符==>,使用格式P1 ==> P2。P1、P2为谓词,整个表达式也为一谓词。当P1为true,P2为false时该谓词返回false,否则返回true。
- 变量引用操作符。\nothing表示空集,\everything表示全集。
1.3数据规格
1.3.1规格变量
在本单元作业中,通过在接口中定义规格变量,来规定要管理的数据。
例如//@public instance model non_null int id;定义了一个规格可见的、引用不为空的、int型实例变量。同理将instance改为static可定义一个静态变量。
1.3.2 /*@spec_public@*/
通过对类中的字段添加/*@spec_public@*/可以指定该变量为规格可见。
1.1.3不变式invariant
//@invariant P;其中P为一个谓词。该语句规定在所有可见状态下,规格所管理的数据都必须满足谓词P。
1.1.4状态变化约束constraint
//@constraint P;其中P为一个谓词。该语句规定数据在前序可见状态与当前可见状态之间需要满足的约束。
1.4方法规格
1.4.1/*@pure@*/
通过对方法修饰/*@ pure @*/,表示该方法仅为查询方法,不会对类的数据做任何修改,使得该方法为规格内可见。
1.4.2行为
一个方法可以有多个行为,行为分为正常行为与异常行为。一个方法也也可以有多个正常行为与异常行为,但他们的前置条件之间不能有交集。正常行为用normal_behavior表示,异常行为用exceptional_behavior表示。多个行为之间用also连接。
1.4.3前置条件
通过使用requires语句实现。格式为requires P。P为一谓词,表示调用该方法时需要满足的限制。一个行为可以有多个requires语句,这些语句之间为且关系,在该行为下需全部被满足。
1.4.4后置条件
通过使用ensures语句实现。格式为ensures P。P为一谓词,表示该方法调用结束时需要满足的限制。一个行为可以有多个ensures语句,这些语句之间为且关系,在该行为下需全部被满足。
1.4.5副作用范围限定
通过assignable语句或者modifiable语句实现。格式为assignable/modifiable v。其中v表示可以被赋值/修改的变量。特别的,当v为\nothing时表示不能赋值\修改任何变量。
1.4.6 signals
signals语句使用在异常行为下。格式为signals E P。E为要抛出的异常包括异常类型与异常变量名称。P为一谓词,表示当谓词满足时抛出异常。当P与该行为前置条件的P相同时,可简化该语句为signals_only E。
2.相关工具链
2.1OpenJML
2.1.1介绍
根据OpenJML官网(http://www.openjml.org/)上的介绍,OpenJML通过使用SMT solver能够为Java程序验证其是否满足JML规格,分为静态(static)检查以及运行时(runtime)检查。
2.2JMLunitng
2.2.1介绍
JMLunitng能通过JML为Java程序提供测试集。
2.2.1本地部署
参考了这篇博客(https://www.cnblogs.com/starmiku/p/10908745.html)的配置。
执行如下指令后
java -jar jmlunitng.jar test/MyGroup.java javac -cp jmlunitng.jar test/*.java java -jar openjml.jar -rac test/MyGroup.java java -cp jmlunitng.jar test.MyGroup_JML_Test
得到如下结果:
总共进行了55次测试,失败了4次。其中3次都是对addPerson传入null参数引起的,但在我们的作业中应当保证了传入的参数不为null。
而测试的样例中集中对边界数据进行了测试。
二、架构设计
1.第一次作业
第一次作业由于对规格不是特别了解,加上对规格的某些语句的工能有所误解,导致第一次作业完全就是按照规格的语句来完成整个设计的。MyPerson中的acquaintance和value,以及MyNetwork中的people都是采用链表结构,对数据的获取和查找基本上都是采用遍历的方式,所以性能较差,担幸好第一次作业在性能上的要求不是十分严格,所以还是逃过了一劫。
1.1类图
2.第二次作业
由于第二次作业比较强调的一点就是性能问题,所以需要对整个作业需要做一次完全的重构。同时将这个社交网络视为以Person为节点,link关系为边,value为边权的无向图。
2.1类图
2.2MyPerson
考虑到acquaintance与value中的数据有一一对应的关系,以及为了提高查找数据的效率,将他们整合成一个以Person为key以value为value的HashMap。
2.3MyGroup
同样为了提高查找效率上的考虑,使用HashSet来作为people数据的组织形式。
对getRelationSum, getValueSum, getConflictSum, getAgeMean, getAgeVar方法,为了不每次调用这些方法都重新遍历一遍数据,所以缓存了realtionSum, valueSum, ageSum, age2Sum(年龄平方和), conflictSum。其中relationSum以及valueSum需要在addPerson以及Network的addRelation时进行更新,其他数据在addPerson时更新即可。另外需要注意的是,ageMean的计算,为了保证与规格中的公式有相同的精确度,需要使用公式ageVar=(age2Sum - 2*mean*ageSum+n*ageSum**2)/n(摘自第十次讨论区乐洋同学的帖子)。
2.4MyNetwork
共用HashMap组织了三个数据:people, groups, peopleInCircle。people以及groups是为了快速通过id查找Person与Group。peopleInCircle无向图的连通分量,peopleInCircle在addRelation时更新。
通过这些储存结构,MyNetwork中大部分方法都只有几个语句,queryCircle方法可以通过查找他们是否在一个连通分量来实现。主要需要注意的方法就是addRelation,当person1与person2在一个Group中的时候需要为为这个Group更新relationSum以及valueSum,并且还需要注意person1与person2是否在同一连通分量,如果不在则需要合并两个连通分量。
3.第三次作业
第三次作业完全基于第二次作业迭代而来。
3.1类图
3.2MyPerson
与第二次作业保持一致。
3.3MyGroup
与第二次作业相比添加了delPerson方法,该方法同时更新people, ageSum, age2Sum, conflictSum, relationSum, valueSum。
3.4MyNetwork
添加了以HashMap组织的money,通过Person的id来查找其money。同时建立了一个内部类Edge来供图算法使用。
queryMinPath使用dijkstra算法,并用优先队列储存边来进行优化。
queryStrongLinked使用tarjan算法来计算点双连通分量以判断两Person是否stronglinked。
三、Bug分析
本单元的bug出现在第三次作业,强测中有两个点出现了CTLE的情况。后来经查看发现这两个点是针对queryMinPath进行测试的,导致CPU时间紧张。
由于自己偷懒就没有再去优化dijkstra算法,于是就选择在Bug修复中直接提交了之前的代码,幸好在评测机资源不紧张的状况下,出现CTLE的两个点都通过了测试。
四、心得体会
由于对JML的阅读还十分有限,所以也谈不上什么心得体会。主要策略就是先界定清楚不同正常行为以及异常行为,然后对每个行为中方法的逻辑进行分析,尝试用规范的逻辑语言去描述它,最后再转化为JML中的语句。