JML语言的理论基础
什么是JML?
注释结构
JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。其中行注释的表示方式为//@annotation,块注释的方式为/* @ annotation @*/。
JML表达式
原子表达式
\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规格中构造一个局部的集合(容器),明确集合中可以包含的元素。(本次单元中以数组的形式表示集合)
操作符
JML表达式中可以正常使用Java语言所定义的操作符,包括算术操作符、逻辑预算操作符等。此外,JML专门又定义了如下四类操作符。
(1) 子类型关系操作符:E1<:E2,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假。
(2) 等价关系操作符:b_expr1<==>b_expr2或者b_expr1<=!=>b_expr2,其中b_expr1和b_expr2都是布尔表达式,这两个表达式的意思是b_expr1==b_expr2或者b_expr1!=b_expr2。
(3) 推理操作符:b_expr1==>b_expr2或者b_expr2<==b_expr1。对于表达式b_expr1==>b_expr2而言,当b_expr1==false,或者b_expr1==true且b_expr2==true时,整个表达式的值为true。
(4) 变量引用操作符:除了可以直接引用Java代码或者JML规格中定义的变量外,JML还提供了几个概括性的关键词来引用相关的变量。\nothing指示一个空集;\everything指示一个全集,即包括当前作用域下能够访问到的所有变量。变量引用操作符经常在assignable句子中使用,如assignable \nothing表示当前作用域下每个变量都不可以在方法执行过程中被赋值。
(本单元实验中并未出现以上四类操作符。绝大部分规格都是用算数操作符和逻辑预算操作符表示)
方法规格
前置条件
前置条件通过requires子句来表示。
后置条件
后置条件通过ensures子句来表示。
副作用范围限定
副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法规格的角度,必须要明确给出副作用范围。JML提供了副作用约束子句,使用关键词assignable或者modifiable。
(其中副作用的范围是jml规格中最容易遗漏和出错的地方)
类型规格
不变式invariant
不变式(invariant)是要求在所有可见状态下都必须满足的特性,语法上定义invariant P,其中invariant为关键词,P为谓词。
状态变化约束constraint
对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。JML为了简化使用规则,规定invariant只针对可见状态(即当下可见状态)的取值进行约束,而是用constraint来对前序可见状态和当前可见状态的关系进行约束。
JML应用工具链的情况
OpenJML:根据JML对实现进行静态的检查
JMLUnitNG:根据JML生成自动的测试样例,用于进行单元测试
但是目前这些工具大部分功能并不完善。
部署SMT Solver
本人使用OpenJML对JML规格进行验证。貌似是由于版本的问题疯狂报错。如下图,检测一半就程序崩溃了。
部署JMLUnitNG
本人由于本机编译器的版本原因始终出现如下报错,在解析HashMap是总是出现报错。查询资料后,出现相同报错原因是由于jdk版本过低,但是本人检查本机jdk版本为1.8,并且重新安装jdk后仍无法解决,所以未完成本机部署。
但通过同学分享得知
对于此自动生成程序多为null或极端边界数据。测试的多为程序在极端边界条件下是否存在溢出、空对象等漏洞,对于复杂逻辑的规格并不能很好地进行测试。
设计框架
这三次作业中由于课程组JML规格给得十分完整,我绝大部分都按照课程组给的框架。只有第三次作业中,为了方便计算最短路径以及找两个人是否有强链接,我创建了Node类和Path类方便算法编写。
代码实现及bug分析
第一次作业:
第一次作业中,主要实现的问题就在于如何选定容器。我采用了ArrayList,由于此次作业没有性能要求,因此我在每次查询时都执行遍历操作,虽然没有问题,但是效率极低。本次作业中也不小心写了出现了一个bug——在isCircle中,我并没有按照规格直接调用isLinked方法判断两个人的关系,而是自己新写了一个判断方法,但是忽略了isLinked中自己和自己的关系,导致测试点大范围出错。这是一个惨痛的教训。
第二次作业:
第二次作业由于加入了性能,我将容器改为了HashMap。在查询上,时间复杂度从O(n)降到了O(1)。但是在getRelationSum和getValueSum两个方法的实现上,采用了双重循环,时间复杂度为O(n²),导致一个测试点超时。为此在修复bug时,我采用在addGroup是就对这两个值进行更新。这样在addGroup方法中时间复杂度为O(n),在两个查询方法上时间复杂度为O(1)。综合来看时间复杂度下降了一个量级。
第三次作业:
第三次作业要完成的功能起始不算多,主要是集中在两个个方法的算法上。在queryMinPath我采用了堆优化的最短路径,queryStrongLinked我先采用bfs搜索出一条可达的路径,再分别删除路径上的点看两个点是否可达。由于选择的算法性能较高,并未出现超时问题,顺利通过测试。
心得体会
JML确实可为我们的代码提供一个规范化的描述。从理论上来说,通过JML规格我们可以证明程序的正确性。但是在实际实现的过程中写出一份完备的JML绝非易事,尤其是对于副作用的定义。在复杂方法中,对于不变量的约束可能就十分地麻烦。
同时对于按照JML规格的代码实现,我们也绝对不能”照搬规格写代码“,虽然代码正确性可以保证,但是代码性能就难以保证,如果只是简单地”翻译规格“,可能会导致出现很多O(n²)甚至更高的复杂度。