BUAA OO 第三单元总结

目录
  • OO 第三单元
    • 一、JML语言的理论基础
    • 二、JML工具链
      • 1.OpenJML
      • 2.jmluniting
    • 三、架构设计
    • 四、bug分析
    • 五、心得体会

OO 第三单元

JML给我上了一课

一、JML语言的理论基础

最开始接触接口的时候,我的直观感受就是“这好像没啥用啊”,它只定义了一些行为的参数和返回值,但是对于实现这些行为帮助不大。转念一想,哦,可以把一些关键的内容写在注释里。这样的注释当然可以用自然语言来写,但是JML提供了另一种思路——用形式化的语言来写注释。虽然这样的方法对于写的人和读的人可能不如自然语言友好,但是它可以避免歧义。避免歧义也很符合计算机的口味,为计算机进行自动化验证甚至自动化生成代码创造了条件。

关于JML的具体内容,请看一个例子:
BUAA OO 第三单元总结_第1张图片
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:
BUAA OO 第三单元总结_第2张图片
JML变量定义

布尔表达式中,可以使用一类特殊的方法——pure方法。pure方法是指纯粹访问性的方法,它不需要前置条件,也不会对对象进行改变,执行一定会正常结束,例如上例中的getPerson:
BUAA OO 第三单元总结_第3张图片
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后,我猜测以下的两个选项可能会有用:
BUAA OO 第三单元总结_第4张图片
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:
BUAA OO 第三单元总结_第5张图片
作业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的工具,以后还要再多看看,至少得知道现在的工具到底实现了啥...
  • 算法学习路还很长。

你可能感兴趣的:(BUAA OO 第三单元总结)