一. JML理论基础及应用工具链
理论基础:
JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。其中行注释的表示方式为 //@annotation,块注释的方式为 /* @annotation @*/ 。
原子表达式:
\result: 表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
\old( expr ): 用来表示一个表达式 expr 在相应方法执行前的取值。
\not_assigned(x,y,...): 用来表示括号中的变量是否在方法执行过程中被赋值。
\not_modified(x,y,...): 与上面的\not_assigned表达式类似,该表达式限制括号中的变量在方法执行期间的取值未发生变化。
\nonnullelements( container ): 表示 container 对象中存储的对象不会有 null。
\type(type): 返回类型type对应的类型(Class)。
\typeof(expr): 该表达式返回expr对应的准确类型。
量化表达式:
\forall表达式: 全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
\exists表达式: 存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
\sum表达式: 返回给定范围内的表达式的和。
\product表达式: 返回给定范围内的表达式的连乘结果。
\max表达式: 返回给定范围内的表达式的最大值。
\min表达式: 返回给定范围内的表达式的最小值。
\num_of表达式: 返回指定变量中满足相应条件的取值个数。
集合表达式:
集合构造表达式:可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。 集合构造表达式的一般形式 为:new ST {T x|R(x)&&P(x)},其中的R(x)对应集合中x的范围,通常是来自于某个既有集合中的元 素,如s.has(x),P(x)对应x取值的约束。
操作符:
JML表达式中可以正常使用Java语言所定义的操作符,包括算术操作符、逻辑预算操作符等。此外,JML 专门又定义了如下四类操作符。
子类型关系操作符: E1<:E2 ,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真, 否则为假。如果E1和E2是相同的类型,该表达式的结果也为真。
等价关系操作符: b_expr1<==>b_expr2 或者 b_expr1<=!=>b_expr2 ,其中b_expr1和b_expr2都 是布尔表达式,这两个表达式的意思是 b_expr1==b_expr2 或者 b_expr1!=b_expr2。
推理操作符: b_expr1==>b_expr2 或者 b_expr2<==b_expr1 。对于表达式 b_expr1==>b_expr2 而言,当 b_expr1==false ,或者 b_expr1==true 且 b_expr2==true 时,整个表达式的值为 true 。
变量引用操作符: 除了可以直接引用Java代码或者JML规格中定义的变量外,JML还提供了几个概括 性的关键词来引用相关的变量。\nothing指示一个空集;\everything指示一个全集,即包括当前作用域 下能够访问到的所有变量。
方法规格:
前置条件(pre-condition): 前置条件通过requires子句来表示: requires P;。
后置条件(post-condition): 后置条件通过ensures子句来表示: ensures P;。
副作用范围限定(side-effects): 副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来 影响。使用关键词 assignable 或者 modifiable 。
signals子句: signals子句的结构为 signals (***Exception e) b_expr;,意思是当 b_expr 为 true 时,方法会抛 出括号中给出的相应异常 e 。signals_only子句是简化的signals子句,后面跟着一个异常类型。signals子句强调在对象状 态满足某个条件时会抛出符合相应类型的异常;而signals_only则不强调对象状态条件,强调满足前置 条件时抛出相应的异常。
类型规格:
不变式invariant: 要求在所有可见状态下都必须满足的特性。
状态变化约束constraint: 对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。
应用工具链:
OpenJML:检查JML的语法正确性。
JMLUnitNG:根据JML自动生成测试用例。
二. 部署SMT Solver
结果如上图所示,OpenJML发现了JML语法的两个错误。
三. 部署JMLUnitNG
使用指令:
结果:
可以看出,10个Failures中,有九个是传入参数为null的hasPerson、addPerson、delPerson;还有一个是开头的racEnabled,emm没太明白怎么回事。
四. 架构设计
本单元的三次作业都是按照给定的接口实现了相应的类而已,也就是说没有进行自己的架构设计。
- 第一次作业
Network中使用HashSet存person,Person中使用两个Arraylist存acquaintance和value,体现出这是没有什么过多的思考就写上去的代码。
- 第二次作业
Network中对于Person和Group各使用HashMap和ArrayList两种容器,分别用于增删查改和遍历,提高性能。
Group中用相同方法存Person。
Person中也用相同的方法存acquaintance和value,与此同时额外增加表明该Person所在的各个Group的相关Relations和Values,便于Group中的求和方法,虽然算是一种使用缓存的思想来提高性能,但很明显没有想到最方便有效的方法。
- 第三次作业
本次作业使用并查集提高查找两个Person之间相连的性能,并增加一个新的类Relation即图中的边,用于堆优化dijkstra求qmp;qsl方法采用不断求路径并与之前求出的路径取交集,查找是否有两条不相交的路;改进了第二次作业求Group中和的缓存方法;其他部分大体与第二次作业相同,money的存储方法也是使用两个容器。
五. bug及修复
- 第一次作业在isCircle中,有并查集的思想但是实现不对,导致强测只过了一个点,没屋。修复时改为bfs。
- 第二次作业虽然使用的缓存方法不够简明,但也在一定程度上提高了性能,没有超时,在公测和互测都没有发现bug。
- 第三次作业在qsl和qmp方法中未先用并查集判断是否isCircle,而且qsl的判断方法不够明智,此处性能很低,强测ctle一个点,互测被hack两个点。修复时增加了提前的判断并更改qsl方法。
六. 心得体会
- 规格可以很好地在设计者和调用者之间形成契约,因此我们在实现代码时,JML的任何一个部分都要仔细阅读,认真体会要求,具体的实现则需要根据实际情况进行选择,不能一味地将JML看成代码就照搬,整体架构、性能都是我们需要考虑的地方。
- 经过本单元的学习,我对JML以及规格都有了一个入门级的体验,这一部分为我们以后写工程代码提供了一种思想层面上的和谐统一。
- 我们学习了Junit单元测试,接触到了对测试严谨全面的要求,相信对以后也是有很大帮助的。
- 总之,本单元学习依旧获益匪浅,希望在日后的学习中能够将这一部分的收获真正融会贯通起来。