一、JML(Java Modeling Language)简介
在最初接触JML过程中,给我的感觉便是代码的形式化表述,我们只需要对照着JML照搬代码即可,但后来发现好像并不是这样,就JML官网的解释来看,The Java Modeling Language (JML) is a behavioral interface specification language that can be used to specify the behavior of Java modules. 虽说是简化了模块表述,但从实际情况来看,它仅仅表述的是正确性,实现的过程其实关联性是不大的,就测评来看,如果全部按照所给的方法和容器也是根本行不通的。
JML表达式 主要分成原子表达式、量化表达式、集合表达式和操作符等,方法规格 主要分成前置条件、后置条件和副作用等。以下均举例说明:
(一)JML表达式
1、原子表达式
表达式 | 解释说明 |
---|---|
\result | 一个非void类型的方法执行所获得的结果,即方法执行后的返回值 |
\old(expr) | 表示一个表达式expr 在相应方法执行前的取值 |
\not_assigned(x,y,...) | 用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为true ,否则返回 false 。用于后置条件的约束,限制一个方法的实现不能对列表中的变量进行赋值 |
2、量化表达式
(1) \forall
: 全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
//针对任意 0<=i
(2) \exists
: 存在量词修饰的表达式,对于给定范围内的元素,存在某个元素满足相应的约束。
//针对0<=i<10,至少存在一个a[i]<0。
(\exists int i; 0 <= i && i < 10; a[i] < 0)
(3) \sum
: 返回给定范围内的表达式的和。
//中间的i是对范围的限制,计算用最右边的参数
(\sum int i; 0 <= i && i < 5; i)
//即0+1+2+3+4==10
(\sum int i; 0 <= i && i < 5; i*i)
//即0+1+4+9+16==30
(4)\max
: 返回给定范围内的表达式的最大值。\min
同理,但表示的是最小值。
(\max int i; 0 <= i && i < 5; i)
3、操作符
(1) b_expr1<==>b_expr2
或 b_expr1<=!=>b_expr2
,等价关系操作符,两边都是布尔表达式
(2) b_expr1==>b_expr2
或 b_expr1<==b_expr2
,推理操作符,相当于离散的->
(3) \nothing
或 \everthing
,变量引用操作符:表示当前作用域访问的所有变量。前者空集,后者全集。变量引用操作符经常在assignable
句子中使用,如 assignable \nothing
表示当前作用域下每个变量都不可以在方法执行过程中被赋值。
(二)方法规格
1、前置条件
对方法输入参数的限制,如果不满足前置条件,方法执行结果不可预测,或者说不保证方法执行结果的正确性。requires P;
其中requires
是JML关键词,表达的意思是“要求调用者确保P为真”。
/*@ also
@ public normal_behavior
@ requires obj != null && obj instanceof Person; 前置条件
@ assignable \nothing;
@ ensures \result == (((Person) obj).getId() == id); 后置条件
@ also
@ public normal_behavior
@ requires obj == null || !(obj instanceof Person); 前置条件
@ assignable \nothing;
@ ensures \result == false; 后置条件
@*/
public /*@pure@*/ boolean equals(Object obj);
2、后置条件
对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,否则执行错误。其中ensures
是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。
3、副作用
/*@
@ ...
@ assignable \nothing; 都不可赋值
@ assignable \everything; 都可赋值
@ assignable elements; elements可赋值
@ assignable elements, max, min; 这仨都可赋值
@*/
(三)其他
1、(/*@ pure @ */)
指不会对对象的状态进行任何改变,也不需要提供输入参数
//@ ensures \result == id;
public /*@pure@*/ int getId();
2、forall
和 exists
表示所有或者存在
/*@ public normal_behavior
@ requires (\exists int i; 0 <= i && i < acquaintance.length;
@ acquaintance[i].getId() == person.getId());
@ assignable \nothing;
@ ensures (\exists int i; 0 <= i && i < acquaintance.length;
@ acquaintance[i].getId() == person.getId() && \result == value[i]);
@ also
@ public normal_behavior
@ requires (\forall int i; 0 <= i && i < acquaintance.length;
@ acquaintance[i].getId() != person.getId());
@ ensures \result == 0;
@*/
public /*@pure@*/ int queryValue(Person person);
3、public normal_behavior
和 public exception_behavior
区分正常功能行为和异常行为
4、signals b_expr
强调满足某个条件抛出相应异常
二、JML相关工具链
1、openJML : 主要用来检查程序程序实现是否满足所需要的规格。
openjml下载网址:http://www.openjml.org/
jmlunitNG下载网址:http://insttech.secretninjaformalmethods.org/software/jmlunitng/
将 Solvers-windows
文件夹 ,jmluniting
jar文件 与 openjml
jar文件和需要测试的程序置于同一文件夹下。
目录树:
由于测试作业程序时报错实在太多,所以借用博客园中一位同学的简单代码进行分析,使用程序:
package demo;
public class Demo {
public static void main(String[] args) {
System.out.println(Demo.add(1, 2));
System.out.println(Demo.sub(2,1));
System.out.println(Demo.div(6, 3));
}
//@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 div(int a, int b) {
return a / b;
}
}
在使用命令行 java -jar .\openjml.jar -exec .\Solvers-windows\z3-4.7.1.exe -esc .\demo\Demo.java
布署 SMT Solver 后出现了以下报错:
.\demo\Demo.java:12: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method add: underflow in int sum
return a + b;
^
.\demo\Demo.java:12: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method add: overflow in int sum
return a + b;
^
.\demo\Demo.java:22: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method div: overflow in int divide
return a / b;
^
.\demo\Demo.java:22: 警告: The prover cannot establish an assertion (PossiblyDivideByZero) in method div
return a / b;
^
.\demo\Demo.java:17: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method sub: underflow in int difference
return a - b;
^
.\demo\Demo.java:17: 警告: The prover cannot establish an assertion (ArithmeticOperationRange) in method sub: overflow in int difference
return a - b;
^
.\demo\Demo.java:17: 警告: The prover cannot establish an assertion (Postcondition: .\demo\Demo.java:15: 注: ) in method sub
return a - b;
^
.\demo\Demo.java:15: 警告: Associated declaration: .\demo\Demo.java:17: 注:
//@ensures \result == a - b;
说明加减可能存在溢出ArithmeticOperationRange
的情况,而除法则可能出现除0 PossiblyDivideByZero
的情况
2 、自动生成测试程序
对文件进行编译:
执行结果:
可以看到,对于一些加减法溢出和除0操作可以检验出来。
但是同时也可以看到,自动测试样例也只是测试在数据极端情况下测试是否抛出异常,因此对于实际应用中的代码正确性检查好像并没有太大的帮助。
作业架构设计
第一次作业中主要按照 JML 规格进行设计,并没有进行设计策略的分析,主要的 iscircle
使用了bfs
方法,由于查询方法较多,所以采用记忆化,在每次 addPerson
和 addRelation
时将所有的数据都进行更新,避免了每次查询时都需要将所有的数据再遍历一遍从而造成时间浪费的情况。
public void addRelation(int id1, int id2, int value) {
Person p1 = people.get(id1);
Person p2 = people.get(id2);
if (p1 != null && p2 != null) {
relationSum = relationSum + 2;
valueSum = valueSum + 2 * value;
}
}
@Override
public void addPerson(Person person) {
for (Person p : people.values()) {
if (person.isLinked(p)) {
relationSum = relationSum + 2;
valueSum = valueSum + 2 * person.queryValue(p);
}
}
people.put(person.getId(), person);
ageSum += person.getAge();
ageeSum += (person.getAge() * person.getAge());
if (tempC == null) {
tempC = person.getCharacter();
} else {
tempC = tempC.xor(person.getCharacter());
}
}
第二次作业中由于测试指令数的增加,所以性能比第一次要求要稍微高一些。关于提升性能,我将HashMap
初始化的值扩大了一些,以避免扩容带来的性能损失,但从第三次作业来看,盲目扩大初始值可能性能反而会下降;其次仍然采用记忆化的方法,其余的与第一次作业并无太大差异。
第三次作业类图:
在本次作业中,我仍然采用了对照JML写代码的习惯,但是如果能将图结构抽象出来,在MyNetwork
中进行边的维护可能会使代码的复用性更强。在主要的方法改动中,将iscircle
改为并查集算法,qmp
采用优先队列的dij
算法,qsl
采用双dfs
算法。
关于复杂度部分,我截取了几个复杂度较高的方法:
主要是qmp和qsl两个方法复杂度较高,如果将图结构抽象出来,在MyNetwork类里维护图结构,复杂度可能会小一些。
Bug分析与修复情况
在第一次作业中,由于对JML的规格实现并不是很熟悉,在一个方法实现中少加了一个判断条件,也导致了没有进入互测的情况。在第二次作业中,没有进行性能的优化导致超时,需要全部实现记忆化以及较为重要的iscircle
需要使用并查集。第三次作业中由于没有进行充分的测试,出现了一些边边角角的错误,结果也不尽如人意,总之由于数据结构的基础较不稳固,对于一些算法的实现可能还是较为陌生。
心得体会
在本单元的三次作业中,主要考察的是对 JML 描述方法的实现,与之前的两个单元有着很大不同,虽然规格已经描述出代码的大致框架,但是对于一些细节还是应该做到仔细审查。同时规格只能是正确性的一种描述,完全按照规格的方式进行代码的实现也是不可行的。关于JML工具链的配置也花费了一些时间,但感觉效果不是很好,只能验证极端条件下的异常情况,所以还是使用Junit自己构造样例来检查更为高效一些。
总之还是很感谢这一单元为大家手写了很多JML规格的助教和老师们,希望之后的单元自己能够有更大的收获吧。