一、JML理论基础及应用工具链
1.JML理论基础
JML是用于对java程序进行规格化设计的一种表示语言,是一种行为接口规范语言。通过JML及其支持工具,可以基于规格自动构造测试用例,并整合SMT solver等工具以静态方式来检查代码实现对于规格的满足情况。
-
JML表达式
(1)原子表达式
- \result表达式:表示一个非void类型的方法执行所获得的结果,即方法执行后的返回值。
- \old(expr)表达式:用来表示一个表达式expr在相应方法执行前的取值。涉及到评估expr中的对象是否发生变化。要用\old把关心的表达式取值整体括起来。
- \not_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值则返回true,否则返回false。
- \not_modified(x,y,...)表达式:与\not_assigned表达式类似,该表达式限制括号中的变量在方法执行期间的取值未发生变化。
- \nonnullelements(container)表达式:表示container对象中存储的对象不会有null。
- \type(type)表达式:返回类型type对应的类型(class)。如\type(boolean)为Boolean.TYPE。
- \typeof(expr)表达式:返回expr对应的准确类型。如\typeof(false) 为Boolean.TYPE。
(2)量化表达式
- \forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
- \exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
- \sum表达式,返回给定范围内表达式的和。
- \product表达式:返回给定范围内的表达式的连乘结果。
- \max表达式:返回给定范围内的表达式的最大值。
- \min表达式:返回给定范围内的表达式的最小值。
- \num_of表达式:返回指定变量中满足相应条件的取值个数。
(3)集合表达式
可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。集合构造表达式的一般形式为:new ST{T x| R(x) && P(x)},其中R(x)对应集合中x的范围,P(x)对应x取值的约束。
(4)操作符
- 子类型关系操作符: E1<:E2,如果类型E1是类型E2的子类型,则该表达式的结果为真,否则为假,如果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==false或者b_expr1==true且b_expr2==true时,整个表达式的值为true。
- 变量引用操作符:\nothing指示一个空集,\everything指示一个全集。
-
方法规格
- 前置条件:requires p;其中requires是JML关键字,表达的意思是 要求调用者确保P为真。
- 后置条件:ensures p;其中ensures是JML关键字,表达的意思是 方法实现者确保方法执行返回结果一定满足谓词p的要求,确保p为真。
- 副作用限定:副作用指方法执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。JML中使用关键词assignable或者modifiable。
-
类型规格
- 不变式invariant:不变式是要求在所有可见状态下都必须满足的特性,语法上定义invariant P。
- 状态变化约束constraint:对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。JML用constraint来对前序可见状态和当前可见状态的关系进行约束。
- 方法与类型规格的关系:大部分情况下,一个类有几种不同类别的方法:静态初始化、有状态静态方法、有状态构造方法、有状态非静态方法。
2、应用工具链
工具链可以参考http://www.eecs.ucf.edu/~leavens/JML//download.shtml。
- AspectJML tool:可以为java和AspectJ程序提供运行时断言检测。
- jml4c:jml4c工具是基于eclipse java编译器构建的JML编译器。
- JMLEclipse:是在Eclipse的JDT编译器基础设施之上开发的JML工具套件的pre-alpha版本。
- Sireum/Kiasan for Java:它是一个面向Java程序单元的基于JML契约的自动验证和测试用例生成工具集。
- JMLUnitNG: 是用于JML注释Java代码的自动化单元测试生成工具。
- JMLOK:使用随机测试来对照JML规范检查Java代码,并为发现的不符合问题提出可能的原因。
二、部署SMT solver
1.openjml
使用openjml进行JML语法检查。我在使用的过程中发现openjml及相关工具链非常不成熟,我先是更换了jdk版本,然后尝试使用idea运行openjml,但是在使用idea的过程中又出现了奇怪的报错,最后换成了eclipse,然后又使用命令行操作,经历几番周折最终才可以成功进行JML语法检查。
实例一:
package test;
public class Test {
public static void main(String[] args) {
}
//@ ensures \result == a + b;
public static int add(int a,int b) {
return a + b;
}
//@ ensures \result == a - b
public static int sub(int a,int b) {
return a - b;
}
//@ ensures \result == a * b;
public static int mult(int a,int b) {
return a * b;
}
}
如代码所示,当JML中少写了分号时,执行命令java -jar openjml\openjml.jar -check -dir test\Test.java
,则会出现如下错误。加上分号则正常。
实例二:
package test;
public class Test {
public static void main(String[] args) {
System.out.print("aa");
}
/*@ public normal_behavior
@ requires a >= 0 && b >= 0;
@ ensures \result == a + b;
@ also
@ public
@ signals (Exception e) a < 0 || b < 0;
*/
public static int add(int a,int b) {
return a + b;
}
}
如代码所示,public 后面缺少exceptional_behavior,执行命令java -jar openjml\openjml.jar -check -dir test\Test.java
,则会出现如下错误,加上exceptional_behavior则正常。
2.solver
我为了方便使用solver,魔改了Person类,然后将openjml.jar,z3-4.7.1.exe,MyPerson.java放在一个目录下,执行命令java -jar openjml.jar -exec z3-4.7.1.exe -esc MyPerson.java
,然后出现下图所示信息。
三、部署JMLUnitNG并进行测试
JMLUnitNG主要用来根据JML语言自动生成数据并进行测试,同样用eclipse。针对Group接口实现自动生成测试样例,我魔改了MyGroup类。为了让JMLUnitNG不报错,我改了一些方法的实现和属性的定义。
依次执行命令
java -jar jmlunitng.jar test\MyGroup.java
javac -cp jmlunitng.jar test\*.java
java -cp jmlunitng.jar test.MyGroup_JML_Test
可以看到输出及目录树如下,通过输出发现,JMLUnitNG主要针对边缘数据进行生成并且测试。
但是我在使用JMLUnitNG的过程中仍然遇到了许多奇怪的问题,最主要的体现就是对java语法的支持不太好,无法支持HashMap,HashSet等容器,只能用普通数组,并且只有用遍历的方法才不会报错。
四、架构设计和模型构建策略
1.第一次作业
第一次作业难度不大,主要是实现Person接口的MyPerson类和实现Network接口的MyNetwork类并根据JML规格实现接口中的方法。MyPerson类主要是管理每个人的属性和存储认识的人以及距离。并提供一系列查询和对比操作。MyNetwork类主要是管理所有的人,并提供查询添加关系、添加人和一系列查询操作。两个类中我均采用ArrayList来存储相应信息,在isCircle方法实现时采用的是dfs。
2.第二次作业
第二次作业在第一次作业的基础上增加了Group接口,实现对人的分组管理,并提供增加人和查询组内信息的操作,如平均数、方差。第二次作业中由于数据量较大,每次遍历Arraylist会耗费比较长的时间,所以我将所有容器均改为了HashMap,将每个人的id作为键,对应的对象作为值,方便根据id快速查找到人。此外,由于第二次作业人数增加,我将isCircle的算法改为并查集,更加节省时间,也不需要多次调用函数防止爆栈。在MyGroup类中,为防止每次查询时都去计算,我用动态更新数据的方式去实现,每次向组里添加人和添加关系的时候都更新数据。
3.第三次作业
第三次作业在第二次作业的基础上增加了在组里删除人的操作和借钱、查询钱的操作,还有比较复杂的求最短路和查询在两个人之前是否存在两条不同的路径。由于我在第二次作业中进行了比较多的优化,所以第三次作业几乎没改变原来的代码,只增添实现了新功能。其中,删除人的实现和增加人的实现正好相反;求最短路径我采用了队列优化的迪杰斯特拉算法,用java自带的priority_queue实现,比较方便;查找是否存在两条路径我采用的是tarjan算法,这个算法我原来并没有接触过,在实现的过程中我认真学习了很长时间,最终将这个算法涉及到的各种方法封装到一个类中,统一管理需要的信息;对于钱的管理仍然使用HashMap容器,方便根据人物id查询钱。
五、bug分析和修复情况
三次作业中均采用对拍的方式,自动生成数据然后给两个程序运行,再将运行结果对比,再生成、运行。这种方式效率比较高,随机覆盖性较好,但是生成的数据比较弱。
1.第一次作业
在第一次作业中出现了致命的bug,主要是在dfs中,遍历完当前人的所有acquaintance之后我从visit集合中删除了这个人,导致时间复杂度爆炸,强测很惨。在修复过程中只需要将visit.remove(id1);
去掉。造成这种情况的主要原因是我在对拍之中只测试了正确性,没有测试时间,并且构造的测试数据不够强,这也让我吸取了教训。
2.第二次作业
第二次作业强测和互测均未出现bug,在互测中也没有发现别人的bug。
3.第三次作业
第三次作业强测和互测均未出现bug,在互测中发现了一个同房间的bug,是对于qgav处理上的问题。
六、心得体会
本单元是我首次接触到规格化设计这种契约化编程方式,在契约化程序设计中,通过JML语言撰写规格,再按照规格的约束进行设计和测试。规格撰写者不需要考虑每个方法内部具体的代码实现,只需要通过规格将功能描述清楚,而程序编写者也只需要考虑如何更好的实现相应接口,而不需要考虑其他部分的耦合,可以很好的避免bug的出现。我觉得规格化设计可以很好的应用于团队开发中,使得团队内部分工明确,不仅可以提高开发效率,也可以避免bug的产生。但是由于JML工具链的不成熟,我在使用openjml和JMLUnitNG等工具时遇到了极大的困难,经过了很长时间之后才解决,而且工具本身并不好用,很多语法都不能支持,也无法很好的覆盖所有错误情况。
此外,我还发现,虽然JML语言对于编程者进行了约束,但是编程者也不能完全按照JML的规格描述进行设计,也需要在JML约束的基础上进行效率的优化。体会比较明显的是本单元的第二次作业,这次作业中,涉及到了大量的查找和计算的操作,我采用HashMap容器根据id更快速查找到person对象,而不是完全按照JML规格上提供的遍历的方法。对于qgav、qgam等操作,我也采用了动态更新的方式,避免了每次查询时都重新进行计算。
总的来说,本单元的契约化编程为程序设计提供了一种新的思路,在以往的程序设计中,我都是在代码的设计时就进行了实现,只关注功能是否正确,没有对每个方法进行严格的约束。而契约化编程可以将程序的设计与实现分离,提供了更加严谨的程序设计方式。