一.JML语言理论基础与应用工具链
1.JML简介
JML(Java Mudeling Language)是一种语言行为规范,通过规范化的语句描述,约束了模块的行为。JML既可以用于规格化设计,又
可以用于针对已有代码的JML规格书写,提高代码的可读性与可维护性。
JML能够便于开发人员之间的交流,同时也能提高程序开发的效率与可维护性。总的来说,JML是对模块行为的精确化描述,有助
于我们有效地发现和改正错误。
2.JML语法
原子表达式:
1.\result
: 表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
2.\old(expr)
: 表示一个表达式expr
在相应方法执行前的取值,该表达式涉及到评估expr
中的对象是否发生变化。需要注意的
是,当expr为引用类型时,修改被引用的内容不会导致expr产生变化,因为expr指向的地址没有变,产生变化的是该地址中的内容。
3.not_assigned(x,y,...)
: 用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为true ,否则返回
false 。用于后置条件的约束,限制一个方法的实现不能对列表中的变量进行赋值。
量化表达式:
1.\forall
: 全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
2.\exists
: 存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
3.\sum
: 返回给定范围内的表达式的和。
4.\min
: 返回给定范围内的表达式的最小值。
5.\max
: 返回给定范围内的表达式的最大值。
方法规格
1.requires
: 前置条件,要求调用者确保其后的条件为真
2.ensures
:后置条件,对方法执行结果的限制。即在满足前置条件的情况下,保证执行结果满足后置条件,否则执行错误。
3.assignable
:副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。
类型规格
invariant
: 不变式,要求在所有可见状态下都必须满足的特性。
3.应用工具链
Openjml : Openjml是用于检查与解析JML规范的工具,其本身并不对规格实现进行检查。不过它一般与SMT Solver(Z3、CVC4等)结
合使用,它将解析出来的JML规范转换为SMT-LIB形式,并将Java程序本身的证明逻辑传给后端的SMT求解器。其解决问题的能力取
其后端的SMT Solver。下载地址:http://www.openjml.org/
JUnit : JUnit是针对Java语言的单元测试框架。可以在其中对各个模块编写自己的单元测试代码,针对性地测试代码中的某一部分。
JMLUnitNg : 可以根据JML规格自动化生成测试用例,不过数据往往十分极端,参考价值不大。下载地址:
http://insttech.secretninjaformalmethods.org/software/jmlunitng/
二.SMT Solver的部署
这里我们用到的SMT Solver是Z3。
Z3的安装和使用可以参考:https://www.cnblogs.com/zwqzj/p/10816245.html
public class Test {
public static void main(String[] args) {
System.out.println(Test.div(10,5));
System.out.println(Test.small(10,5));
}
//@ensures \result == a / b;
public static int div(int a, int b) {
return a / b;
}
//@ensures \result == (sum char i; i >= 'a' && i <= 'z' && name.contains(i); 1)
public static int small(String name) {
int sum = 0;
for(char x : String name) {
if (x <= 'z' && x >= 'a') {
sum++;
}
}
return sum;
}
}
将Z3安装之Openjml对应目录下,并配置好相应的环境变量后,在命令行输入如下指令即可开始测试:
java -jar C:\Users\Desktop\openjml\openjml.jar -esc -prover cvc4 -exec C:\Users\Desktop\openjml\Solvers-windows\cvc4-1.6.exe C:\Users\IdeaProjects\demo\src\Test.java
使用Openjml与Z3求解器对上述代码进行测试,得到两条INVALID报错,提示我们div无法处理b = 0的情况以及small在name = null的情况时无法得到正确结果。
三.JMLUnitNg的部署
直接将JMLUnitNg压缩包和代码放在Openjml目录下即可开始运行。
针对Group的测试结果如下:
可以看出JMLUnitNg生成的测试样例非常极端,基本就是null或0或MAX_INT,我个人认为参考价值不大。。。
四.代码架构分析
第九次作业
本次作业是我们首次接触JML,实现并不困难,由于是第一次作业,并没有特别复杂的函数,唯一略微复杂的isCircle也因为数据
大小限制被我直接一个三重循环水了过去。其它的函数实现基本上直接照着JML规格实现就没有问题。不过在数据规格上,我由于采用
了ArrarList,导致后面就出现了时间复杂度较高的情况。
第十次作业
本次作业最大的问题在于时间复杂度,要求我们在类实现的过程中建立缓存,而不是完全照着JML规格写,JML规格只是对模块功
能的描述,切记不能把描述当成真正的实现方式。如这一次,如果每个qa、qc都如JML描述的遍历一遍Person来计算的话,就会导致很
多重复的计算,大大地增加了时间复杂度。因此我们应该针对模块功能,作出另外一种实现,即建立age和conflict的缓存机制,从而不
需要每次查询的时候反复遍历Arraylist(这是十分耗时的)。还有一个就是isCircle,n^3的复杂度在更大的数据规模下明显不再可行,我
改成了普通的dfs,这也提醒我在编写程序的时候一定要注意可扩展性,而不是利用目前的测试数据来偷懒。
第十一次作业
本次作业的难度在于有关图论的算法分析。重温了迪杰斯特拉算法(求解最短路径),并且新学习了塔扬算法(求解双连通分量)。
最困难的地方应当是tarjan算法的分析了(不过这里可以用若干次删点来代替,时间复杂度勉强过关),tarjan关于点双连通分量的求解确
实需要分析很久才能彻底明白。其次是并查集的应用,由于本次作业中不存在deleRelation或delePersonFromNetwork,所以在求解连通
分支数量的时候可以使用并查集,这样能够将O(n^2)的查询复杂度降到O(1)(不过在addPerson和addRelation的时候会变慢),根据强测
的数据分析来看,使用并查集无疑在时间复杂度上优化了很多。
五.bug分析与修复
值得分析的bug主要出现在tarjan算法和时间复杂度的问题上。一则是我对tarjan算法的理解还不够透彻,导致在写点双连通分量的求解
时出现了错误,具体是因为在退栈的时候把割点也退了出去。而事实上,割点是同时存在于两个点双连通分量中的,因此出现了bug。
二则是第十次作业中由于偷懒,没有使用缓存机制,而是盲目地按照JML规格的描述进行实现,将规格变成了具体实现(规格应当只是
对于功能的规范化描述,而不是对功能的具体实现),导致时间复杂度出现了问题,出现了TLE,在将agesum等改用缓存机制后显著降低了
时间复杂度,修复了tle的问题。
六.心得体会
1.JML确实能大大降低开发过程中遇到的协作难度,利用规范化的语言能够清晰地描述模块的功能,这在功能封装的时候是十分有利的。
2.JML是规范化的描述,只是对自然语言描述的一种程序化表达。绝不能将模块的JML描述当成具体实现,JML只是为了告诉编程者模块
的功能规范,建立一个契约化的程序编写,具体的优化与实现还需要自己去思考。
3.作为一名合格的编程者,需要熟悉各种经典的算法,再不济也要理解算法的基本思路。像这一次的tarjan算法,我就在还没有彻底理解的
情况下开始写了,导致在实现过程中出现了许多细节性的问题。同时我们还应该掌握各种优化程序性能的手段,如并查集的使用、缓存机制的
使用等等,不能只追求功能上的正确,更要考虑性能是否过关!
2