BUAA_OO_UNIT3单元总结

BUAA_OO_UNIT3单元总结

1、JML理论基础梳理以及应用工具链情况

理论基础

原子表达式

\result表达式:方法执行的结果

\old( expr )表达式:表达式 expr 方法执行前的值
\not_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值

\not_modified(x,y,...)表达式:括号中变量的取值在方法执行过程中未发生变化

\nonnullelements( container )表达式: container中存储的对象不会有 null

\type(type)表达式:返回type对应的类型

\typeof(expr)表达式:返回expr对应的类型

量化表达式

\forall表达式:全称量词

\exists表达式:存在量词

\sum表达式:求和

\max表达式:最大值

\min表达式:最小值

方法规格

前置条件(pre-condition):requires P,要求调用者确保P为真

后置条件(post-condition):ensures P,方法实现者确保方法执行的返回结果一定确保P为真

副作用范围限定(side-effects): assignable 或者modifiable,方法在执行过程中会修改对象的属性数据或者类的静态成员数据

纯粹访问性的方法: pure,不改变任何东西

抛出异常子句: signals

应用工具链

  1. OpenJML:可以检查JML文档规格语法,其中SMT Solver可以验证代码是否符合规格
  2. JMLUnitNG:一款基于JML的单元测试工具,可以自动生成测试用例
  3. Junit:用于进行单元测试,可编写和可重复运行的自动化测试

后言

jml规格的出现无疑是众多程序员的福音,可以确保在无二义性的情况下描述一个方法、描述一个类,可以保证实现者正确的实现需求者想要的功能(实现者正确实现了JML规格的情况下)。

但是jml规格虽好,但是想写出一个正确的jml规格却并不简单。面对复杂的功能需求时,jml规格往往会变得相当复杂,不仅写起来容易出错,出现漏限制条件的情况,实现者在阅读规格时也会十分头疼。同时jml相关工具链并十分垃圾不成熟,使用起来并不好用,因此在验证代码是否符合规格时还是需要大量的手动测试。

2、SMT Solver验证

为了方便进行验证,我们将Person类进行魔改,代码如下

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;

public class MyPerson{
    private /*@ spec_public @*/ int id;
    private /*@ spec_public @*/ String name;
    private /*@ spec_public @*/ BigInteger character;
    private /*@ spec_public @*/ int age;
    private /*@ spec_public @*/ ArrayList<MyPerson> acquaintance = new ArrayList<>();
    private /*@ spec_public @*/ HashMap<Integer,Integer> value = new HashMap<>();

    public MyPerson(int id, String name, BigInteger character, int age) {
        this.id = id;
        this.name = name;
        this.character = character;
        this.age = age;
    }

    public void addlink(MyPerson p,int v) {
        acquaintance.add(p);
        value.put(p.getId(),v);
    }

    public ArrayList<MyPerson> getAc() {
        return acquaintance;
    }

    //@ ensures \result == id;
    public int getId() {
        return this.id;
    }

    //@ ensures \result.equals(name);
    public String getName() {
        return this.name;
    }

    //@ ensures \result.equals(character);
    public BigInteger getCharacter() {
        return this.character;
    }

    //@ ensures \result == age;
    public int getAge() {
        return this.age;
    }

    /*@ also
      @ public normal_behavior
      @ requires obj != null && obj instanceof MyPerson;
      @ assignable \nothing;
      @ ensures \result == (((MyPerson) obj).getId() == id);
      @ also
      @ public normal_behavior
      @ requires obj == null || !(obj instanceof MyPerson);
      @ assignable \nothing;
      @ ensures \result == false;
      @*/
    public boolean equals(Object obj) {
        if (obj != null) {
            if (obj instanceof MyPerson) {
                return ((MyPerson)obj).getId() == this.id;
            }
        }
        return false;
    }

    /*@ public normal_behavior
      @ assignable \nothing;
      @ ensures \result == (\exists int i; 0 <= i && i < acquaintance.length;
      @                     acquaintance[i].getId() == MyPerson.getId()) || MyPerson.getId() == id;
      @*/
    public boolean isLinked(MyPerson MyPerson) {
        if (MyPerson.getId() == this.id) {
            return true;
        }
        return value.containsKey(MyPerson.getId());
    }

    /*@ public normal_behavior
      @ requires (\exists int i; 0 <= i && i < acquaintance.length;
      @          acquaintance[i].getId() == MyPerson.getId());
      @ assignable \nothing;
      @ ensures (\exists int i; 0 <= i && i < acquaintance.length;
      @         acquaintance[i].getId() == MyPerson.getId() && \result == value[i]);
      @ also
      @ public normal_behavior
      @ requires (\forall int i; 0 <= i && i < acquaintance.length;
      @          acquaintance[i].getId() != MyPerson.getId());
      @ ensures \result == 0;
      @*/
    public int queryValue(MyPerson MyPerson) {
        if (value.containsKey(MyPerson.getId())) {
            return value.get(MyPerson.getId());
        }
        return 0;
    }

    //@ ensures \result == acquaintance.length;
    public int getAcquaintanceSum() {
        return acquaintance.size();
    }

    /*@ also
      @ public normal_behavior
      @ ensures \result == name.compareTo(p2.getName());
      @*/
    public int compareTo(MyPerson p2) {
        return this.name.compareTo(p2.getName());
    }
}

(对于本地配置设置不好的同学这里贴上一份网页openjml的地址https://www.rise4fun.com/OpenJMLESC/)

(白色为网页测试结果,黑色为本地测试结果,一致)

对于person类我们可以看到会出现一些报错信息,但是实际上代码本身的功能实现应该是完全符合了规格要求的,并且大部分报错是因为Array类型和Arraylist类型导致的,当我们将Arraylist更改为普通的数组时,代码和检测结果如下

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;

public class MyPerson{
    private /*@ spec_public @*/ int id;
    private /*@ spec_public @*/ String name;
    private /*@ spec_public @*/ BigInteger character;
    private /*@ spec_public @*/ int age;
    private /*@ spec_public @*/ MyPerson[] acquaintance = new MyPerson[1000];
    private /*@ spec_public @*/ HashMap<Integer,Integer> value = new HashMap<>();

    public MyPerson(int id, String name, BigInteger character, int age) {
        this.id = id;
        this.name = name;
        this.character = character;
        this.age = age;
    }

    //@ ensures \result == id;
    public int getId() {
        return this.id;
    }

    //@ ensures \result.equals(name);
    public String getName() {
        return this.name;
    }

    //@ ensures \result.equals(character);
    public BigInteger getCharacter() {
        return this.character;
    }

    //@ ensures \result == age;
    public int getAge() {
        return this.age;
    }

    /*@ also
      @ public normal_behavior
      @ requires obj != null && obj instanceof MyPerson;
      @ assignable \nothing;
      @ ensures \result == (((MyPerson) obj).getId() == id);
      @ also
      @ public normal_behavior
      @ requires obj == null || !(obj instanceof MyPerson);
      @ assignable \nothing;
      @ ensures \result == false;
      @*/
    public boolean equals(Object obj) {
        if (obj != null) {
            if (obj instanceof MyPerson) {
                return ((MyPerson)obj).getId() == this.id;
            }
        }
        return false;
    }

    /*@ public normal_behavior
      @ assignable \nothing;
      @ ensures \result == (\exists int i; 0 <= i && i < acquaintance.length;
      @                     acquaintance[i].getId() == MyPerson.getId()) || MyPerson.getId() == id;
      @*/
    public boolean isLinked(MyPerson MyPerson) {
        if (MyPerson.getId() == this.id) {
            return true;
        }
        return value.containsKey(MyPerson.getId());
    }

    /*@ public normal_behavior
      @ requires (\exists int i; 0 <= i && i < acquaintance.length;
      @          acquaintance[i].getId() == MyPerson.getId());
      @ assignable \nothing;
      @ ensures (\exists int i; 0 <= i && i < acquaintance.length;
      @         acquaintance[i].getId() == MyPerson.getId() && \result == value[i]);
      @ also
      @ public normal_behavior
      @ requires (\forall int i; 0 <= i && i < acquaintance.length;
      @          acquaintance[i].getId() != MyPerson.getId());
      @ ensures \result == 0;
      @*/
    public int queryValue(MyPerson MyPerson) {
        if (value.containsKey(MyPerson.getId())) {
            return value.get(MyPerson.getId());
        }
        return 0;
    }

    //@ ensures \result == acquaintance.length;
    public int getAcquaintanceSum() {
        return acquaintance.length;
    }

    /*@ also
      @ public normal_behavior
      @ ensures \result == name.compareTo(p2.getName());
      @*/
    public int compareTo(MyPerson p2) {
        return this.name.compareTo(p2.getName());
    }
}


可以看见只剩下一个类型不同的报错,其他方法的验证可以算是成功

由此可见openjml使用起来并不是很好用,连Array与Arraylist的智能转换都做不到

3、JMLUnit测试

看来本次博客作业的真正幕后大BOSS是jmluniting

最开始用电脑的命令行跑总是出现奇怪的错误,最后在某大佬的推荐下改用idea的命令行居然非常顺利的成功了。

我们从上面的测试结果可以看出jmlunit进行测试时着重测试了许多边界情况,而面对其中一些方法的边界情况测试我也是fail掉了,可以说明有的特殊情况还是没有考虑到(虽然可能不影响结果)

由此我们可以得出其实jmlunit测试还是挺有必要的,帮助检查边界情况,但是仅仅有jmlunit测试是远远不够的,我们还是需要借助junit对自己的代码进行全覆盖的单元测试,来保证代码的正确性。

4、架构设计分析

第一次作业

第一次作业除了isCircle方法以外,其他方法都还挺简单的,丝毫考虑过优化发展的我,选择了全部按照规格描述的方法直接写,全部采用Arraylist来实现。然后对于isCircle方法,由于觉得dfs/bfs写起来太麻烦不如并查集简单,于是新创建了一个类并将并查集相关操作封装进去,查询时直接利用并查集来查询即可(这也是第一次作业做的唯一好的地方了)。

第二次作业

相比于第一次作业,第二次作业增加了一个Group类,并且在Network类中添加了许多查询group中people各种相关数据计算的方法。

由于数据量较大,为了提高速率我在每个类创建了一个Hashset用于实现查找操作,代替了原来的便利Arraylist操作。同时在group增加人时,更新该group的年龄和、年龄平方和、关系和等等数据,并且利用相关数学公式从而实现各种数据查询的时候操作为o(1)复杂度。

第三次作业

第三次作业相对于第二次作业,在group中添加了删人的方法,相应的需要我们在删人时即使实现相应数据的更新。

本次作业的重头戏在于三个方法,queryMinPath、queryStrongLinked以及queryBlockSum

首先是读懂这三个方法的jml,这三个方法的规格都比较长而且比较复杂,需要耐心的阅读。(不过通过名字就已经可以猜出个大概,然后按照相应方向理解还是比较简单)

其中最好解决的是queryBlockSum,由于我本身有一个并查集类,因此在统计块数时十分方便,在加人和连人是对blocksum进行+-操作即可。每增加一个新人,blocksum++,当连接v1和v2时,如果fa(v1)!=fa(v2),blocksum--。最后查询blocksum时直接返回即可。

queryMinPath就是一个查询最短路径长度的操作,我采用的算法是堆优化的迪杰斯特拉,将最短路算法封装成一个方法,查询最短路时直接调用方法即可。为了方便实现堆的排序,我新建了一个point类,拥有两个属性,一个是点的id,另一个是起点到该点的距离。

queryStrongLinked是相对而言最难解决的一个函数,倒不是说方法不好找,而是tarjan算法之前并未接触过,学起来比较麻烦,基本思路是采用tarjan寻找点双连通分量并且保存下来,最后看两点是否同时存在同一个点双连通分量当中。同样的,也是将tarjan板子封装成一个方法,增添一个查询即可。

5、代码bug和修复情况

三次作业中,只有第二次作业的强测出现了问题,其余强测互测均无问题。

在第二次作业的完成过程中,在关心时间复杂度时只考虑到了各种数据的计算,导致其他方法的实现没有仔细检查,结果导致queryNameRank方法中出现了循环嵌套,强测前18个点超时,最终只有20分。

不得不承认,第二次作业完成后的检查并没有做的很好,在和同学对拍时每组数据都会慢一点的情况也并没有引起我的重视,过于轻敌,导致直接裂开。这次作业也算是敲响了一个警钟,不管作业简不简单,该完成的检查还是得做好。

6、心得体会

JML规格对于我们写代码来说真的是非常方便,但是对于实现者而言,不能无脑照着规格写,而是应该在理解了规格的需求后考虑时间和空间开销,并且进行自己的架构。本单元的第二次作业中,就是因为无脑按照规格写,导致出现了循环嵌套,最终被一些数据卡掉时间。

JML规格写的时候是不用考虑实现的,更多的是关注于需要实现的效果,而如何去实现则是实现者需要考虑的事情,JML规格很好的将程序的设计和实现分离,而这种功能在面对团队合作开发时会显得尤为重要。

在实验课上我们体验了一次写jml规格,写起来还是挺难的,不知道怎么写才能写全,目前我们掌握的好一点的还是读jml代码,而在写jml代码这条路上还有很长很长的路要走(由此可见助教是多么牛逼)。

猜你喜欢

转载自www.cnblogs.com/yz2020/p/12940726.html