- OO 第三单元
- 一、JML语言的理论基础
- 二、JML工具链
- 1.OpenJML
- 2.jmluniting
- 三、架构设计
- 四、bug分析
- 五、心得体会
OO 第三单元
JML给我上了一课
一、JML语言的理论基础
最开始接触接口的时候,我的直观感受就是“这好像没啥用啊”,它只定义了一些行为的参数和返回值,但是对于实现这些行为帮助不大。转念一想,哦,可以把一些关键的内容写在注释里。这样的注释当然可以用自然语言来写,但是JML提供了另一种思路——用形式化的语言来写注释。虽然这样的方法对于写的人和读的人可能不如自然语言友好,但是它可以避免歧义。避免歧义也很符合计算机的口味,为计算机进行自动化验证甚至自动化生成代码创造了条件。
这个例子中的方法是把一个编号为id1的person加入到编号为id2的group中。从这个例子可以看出,一个方法的JML由0或更多个normal_behavior和0或更多个exceptional_behavior组成,之间用also连接。每个normal_behavior由requires(满足什么条件才执行这个behavior), assignable(这个behavior可能会修改什么对象), ensures(这个behavior执行后能保证什么结果)构成;而每个exceptional_behavior由requires, assignable, signals(满足特定条件需要抛出特定异常)构成。
requires, ensures, signals都会跟随一个布尔表达式,其中涉及到一些JML的规则。
布尔表达式中的变量,在接口的最开始定义,例如上例中的people和groups:
JML变量定义
布尔表达式中,可以使用一类特殊的方法——pure方法。pure方法是指纯粹访问性的方法,它不需要前置条件,也不会对对象进行改变,执行一定会正常结束,例如上例中的getPerson:
JML pure方法
布尔表达式中还有一些特殊的表达式,例如\result, \exists, \forall等等,不难理解,在此不赘述了。有一个相对特殊的是\old,表示方法调用前的情况。
除了上例中针对方法的方法规格,还有针对类的类规格,主要包括invariant和constraints两种。
invariant是一个数据在可见状态下应该满足的要求。可见状态在此不详述了,大致就是说,在会修改状态的方法执行期间是不可见的,允许暂时违背invariant。
constraints描述一个数据如果要变化,怎么变。
invariant和constraints例如:
private /*@spec_public@*/ long counter;
//@ invariant counter >= 0;
//@ constraint counter == \old(counter)+1;
回到目录
二、JML工具链
1.OpenJML
在查阅了OpenJMLUserGuide后,我猜测以下的两个选项可能会有用:
OpenJML的两个选项
这里边一共提到了三种检查:type-checking, static checking, checking at runtime.
参照大家的讨论,我对Group进行了一些修改和简化,以适应OpenJML的要求:
package test;
import java.math.BigInteger;
import java.util.ArrayList;
public class Group {
public int id;
public Person[] people;
public int peopleSum;
public int relationSum;
public int valueSum;
public BigInteger conflictSum;
public int ageSum;
public Group(int id) {
this.id = id;
people = new Person[1024];
peopleSum = 0;
relationSum = 0;
valueSum = 0;
conflictSum = BigInteger.ZERO;
ageSum = 0;
}
//@ ensures \result == id;
public /*@pure@*/ int getId(){
return id;
}
/*@ also
@ public normal_behavior
@ requires obj != null && obj instanceof Group;
@ assignable \nothing;
@ ensures \result == (((Group) obj).getId() == id);
@ also
@ public normal_behavior
@ requires obj == null || !(obj instanceof Group);
@ assignable \nothing;
@ ensures \result == false;
@*/
public /*@pure@*/ boolean equals(Object obj){
if (this == obj) {
return true;
}
if (!(obj instanceof Group)) {
return false;
}
Group group = (Group) obj;
return getId() == group.getId();
}
public void addPerson(Person newPerson){
people[peopleSum] = newPerson;
peopleSum += 1;
for (int i = 0; i < peopleSum; i++) {
Person person = people[i];
if (newPerson.isLinked(person)) {
if (newPerson.equals(person)) {
relationSum += 1;
} else {
relationSum += 2;
valueSum += newPerson.queryValue(person) + person.queryValue(newPerson);
}
}
}
conflictSum = conflictSum.xor(newPerson.getCharacter());
ageSum += newPerson.getAge();
}
//@ ensures \result == (\exists int i; 0 <= i && i < people.length; people[i].equals(person));
public /*@pure@*/ boolean hasPerson(Person person){
for(int i = 0; i < peopleSum; i++) {
if(people[i].equals(person)) {
return true;
}
}
return false;
}
/*@ ensures \result == (\sum int i; 0 <= i && i < people.length;
@ (\sum int j; 0 <= j && j < people.length && people[i].isLinked(people[j]); 1));
@*/
public /*@pure@*/ int getRelationSum(){
return relationSum;
}
/*@ ensures \result == (\sum int i; 0 <= i && i < people.length;
@ (\sum int j; 0 <= j && j < people.length &&
@ people[i].isLinked(people[j]); people[i].queryValue(people[j])));
@*/
public /*@pure@*/ int getValueSum(){
return valueSum;
}
/*@ public normal_behavior
@ requires people.length > 0;
@ ensures (\exists BigInteger[] temp;
@ temp.length == people.length && temp[0] == people[0].getCharacter();
@ (\forall int i; 1 <= i && i < temp.length;
@ temp[i] == temp[i-1].xor(people[i].getCharacter())) &&
@ \result == temp[temp.length - 1]);
@ also
@ public normal_behavior
@ requires people.length == 0;
@ ensures \result == BigInteger.ZERO;
@*/
public /*@pure@*/ BigInteger getConflictSum(){
return conflictSum;
}
/*@ ensures \result == (people.length == 0 ? 0 :
@ ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length));
@*/
public /*@pure@*/ int getAgeMean(){
int n = peopleSum;
if (n == 0) {
return 0;
} else {
return ageSum / n;
}
}
}
首先执行java -jar PATH/openjml.jar -exec PATH/openjml/Solvers-macos/z3-4.7.1 -esc test/Person.java test/Group.java
执行后,报了一个错误:
错误: 不可比较的类型: int和INT#1
/*@ ensures \result == (people.length == 0 ? 0 :
^
其中, INT#1是交叉类型:
INT#1扩展Number,Comparable
查了一下,交叉类型是Java的一种语言特性,暂时还没弄懂这块为啥出这个,留个坑,姑且认为没啥问题。
然后执行java -jar /Users/xuhaoyu/Downloads/openjml/openjml.jar -rac test/Person.java test/Group.java
看结果,多了这三个“注”:
注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression
@ (\sum int j; 0 <= j && j < people.length && people[i].isLinked(people[j]); 1));
^
注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression
@ (\sum int j; 0 <= j && j < people.length &&
^
注: Runtime assertion checking is not implemented for this type or number of declarations in a quantified expression
@ ensures (\exists BigInteger[] temp;
^
可能是说对\sum, \exists这种不太适用吧...
为了弄清楚几种check到底check了啥,我试着把代码改错一些地方。
如果JML语法错(比如少个括号,少个分号),会报错误,我猜测这是type-checking.
如果测试如下代码:
//@ ensures \result == 0;
public /*@pure@*/ int getZero(){
return 1;
}
会报警告,我猜测这是static checking.
最后checking at runtime还没弄懂(再次留坑),现在只知道这种check是会编译出class文件的,前两种不会。
回到目录
2.jmluniting
执行以下四条指令:
javac test/Group.java
java -jar jmlunitng.jar test/Group.java
javac -cp jmlunitng.jar test/*.java
java -cp jmlunitng.jar test.Group_JML_Test
结果:
Failed: racEnabled()
Passed: constructor Group(-2147483648)
Passed: constructor Group(0)
Passed: constructor Group(2147483647)
Failed: <>.addPerson(null)
Failed: <>.addPerson(null)
Failed: <>.addPerson(null)
Passed: <>.equals(null)
Passed: <>.equals(null)
Passed: <>.equals(null)
Passed: <>.equals(java.lang.Object@2280cdac)
Passed: <>.equals(java.lang.Object@4fccd51b)
Passed: <>.equals(java.lang.Object@60215eee)
Passed: <>.getAgeMean()
Passed: <>.getAgeMean()
Passed: <>.getAgeMean()
Passed: <>.getConflictSum()
Passed: <>.getConflictSum()
Passed: <>.getConflictSum()
Passed: <>.getId()
Passed: <>.getId()
Passed: <>.getId()
Passed: <>.getRelationSum()
Passed: <>.getRelationSum()
Passed: <>.getRelationSum()
Passed: <>.getValueSum()
Passed: <>.getValueSum()
Passed: <>.getValueSum()
Passed: <>.hasPerson(null)
Passed: <>.hasPerson(null)
Passed: <>.hasPerson(null)
对于有参数的方法,看起来就是传一些极端参数进行测试。
对于没参数的部分,没弄懂是怎么测的,从生成的那些java文件里也没看出啥...我尝试getRelationSum()改成直接return 12345,重新编译测试也能pass,感觉非常疑惑...现在只能推测是这个方法的JML中包含工具不认的东西。
回到目录
三、架构设计
这一单元由于JML给的比较详细,加上有运行时间的限制,所以可以自由发挥的地方不是很多。
- 第一次作业我认为主要的抉择点就是容器的选择。读完JML之后我发现可以在ArrayList和HashMap中选一个,简单估计了时间复杂度之后我选择了ArrayList,这是因为ArrayList和JML中的静态数组比较接近,方法的实现基本上就是翻译JML,而这次作业时间给得比较宽,频繁遍历ArrayList也完全能接受。
- 第二次作业对时间有一定要求,我首先把容器换成了ArrayList和HashMap的冗余存储。此外还有对数据进行适当的缓存,来在查询时节约时间。我选择的是在加人的时候维护relationSum, valueSum, conflictSum, ageMean, ageVar;在加关系的时候维护relationSum, valueSum. 其中对于ageMean和ageVar的维护采取了每次加人都循环遍历重新计算的方法,经过估计这样计算不会超时,且可以不用考虑误差的问题。
- 第三次作业主要的精力都花在解决queryMinPath, queryStrongLinked, queryBlockSum上。其中queryMinPath比较简单,是我们熟悉的Dijkstra,经讨论区同学提醒使用堆优化,来减少获取最近点所需的时间。queryBlockSum也不难,选用的方法是遍历所有人,如果已经访问过他则跳过,否则从他开始bfs,把所有bfs到的人标记为访问过。最后就是queryStrongLinked,对我来说还是比较难的,花了两天时间也没整出来。
- 研讨课上还提到了一个点,涉及到比较复杂算法的部分可以单独提取出来,这是我当时没有想到的。
回到目录
四、bug分析
这一单元的bug还是比较严重的。
第一次作业强测5/100,出错原因请看addRelation的JML:
作业1 addRelation方法部分JML
首先看这个normal_behavior,assignable \nothing,那好像就是不用管。再看这个exceptional_behavior,id没有的时候PersonIdNotFoundException没啥问题,如果是(getPerson(id1).isLinked(getPerson(id2)) && id1 != id2),id1==id2的情况上边normal_behavior已经包含了,那就可以把id1 != id2扔掉了。结果这样就会出问题,id1==id2的时候本来应该啥都不干结果抛出了EqualRelationException. 避免这种错误有几个方法:一是对条件完全不化简,我觉得不是很好,会增加一些冗余的判断;二是对于assignable \nothing的分支直接return,我觉得很好,贴合assignable \nothing的语义;三是在写assignable \nothing之外的分支时不考虑assignable \nothing排除掉的条件,这样assignable \nothing事实上不会进到任何一个分支,我觉得也不好,化简容易出错。
自己测试没测出这个问题是因为我潜意识中把addRelation当成了一个“准备数据”的方法,测试的时候没有遍历它的分支,这种想法是很可怕的,以后应该注意。
第二次作业强测20/100,引发了我对于测试的思考。
这次出错的原因在于,我一看getRelationSum的JML,得到的理解是:一个关系要算两次。我忽略了自己和自己的关系只算一次这个情况,可以说是对于功能的认识不够。按说这个bug只要自己跑了getRelationSum就能发现,但我却没测出来。究其原因,我在测试时采用的是手搓的数据,而作为对比的正确结果是自己人脑跑出来的答案,其实是几乎无效的测试。这个bug给我带来了一些启发:测试至少要把样例过一遍,保证最基本的功能不出错;可以和同学进行对拍作为答案;从研讨课中学到,很多经典问题都有对应的python库,可以用python程序作为标程。但这几个方法有个问题,就是只对我们的作业适用,万一以后弄个啥,没有样例,没有别人的程序,也没有python库,那咋办?我现在能想到的办法就是翻译JML,用最接近JML的暴力代码作为对照。当然这样也有问题,如果方法比较复杂可能写不出暴力代码,或者暴力代码时间太长跑不出来,所以实际情况中可能还要几种办法综合使用。
第三次作业强测90/100,问题不大。
出错的点是queryStrongLinked,由于tarjan算法没看懂,又没有想出其他办法,最后只能采取遍历所有路径,分别删掉每条路看看还有没有路来判断是否点双联通。由于这样必须在遍历完所有路径之后才能判断非点双联通,显然的问题就是在路径很多且非点双联通时会超时,强测只错了两个点感谢数据组手下留情。后来凭借讨论区助教给出的算法修复了这个bug,不过tarjan的坑还是需要找时间填一下。
回到目录
五、心得体会
- JML在多人合作的时候感觉会很有用,如果没有类似的规范化的语言,合作的效率会大打折扣。
- 经过这个单元学会了读JML并进行设计的基本方法,总结来说就是一方面要听JML的话,另一方面自己要对被实现的功能有想法,不能只是埋头写。
- 没时间了没能更深入学习JML的工具,以后还要再多看看,至少得知道现在的工具到底实现了啥...
- 算法学习路还很长。