OO第三单元总结

OO第三单元总结

一、JML理论基础及应用工具链

1.JML 理论基础

  • JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。通过JML及其支持工具,不 仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满 足情况。

  • 通过参考 JML Level 0手册 ,组成内容如下:

    • (1)注释结构:

      • JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。其中行注 释的表示方式为 //@annotation ,块注释的方式为 /* @ annotation @*/ 。按照Javadoc习惯,JML 注释一般放在被注释成分的紧邻上部。
    • (2)JML表达式:

      • 原子表达式:
        • \result表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。
        • \old( expr )表达式:用来表示一个表达式 expr 在相应方法执行前的取值。
        • \not_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值。
        • \not_modified(x,y,...)表达式:与上面的\not_assigned表达式类似,该表达式限制括号中的变量在方法 执行期间的取值未发生变化。
        • \nonnullelements( container )表达式:表示 container 对象中存储的对象不会有 null。
        • \type(type)表达式:返回类型type对应的类型(Class)。
        • \typeof(expr)表达式:该表达式返回expr对应的准确类型。
      • 量化表达式:
        • \forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。
        • \exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。
        • \sum表达式:返回给定范围内的表达式的和。
        • \product表达式:返回给定范围内的表达式的连乘结果。
        • \max表达式:返回给定范围内的表达式的最大值。
        • \min表达式:返回给定范围内的表达式的最小值。
        • \num_of表达式:返回指定变量中满足相应条件的取值个数。
      • 集合表达式:
        • 集合构造表达式:可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。
        • 集合构造表达式的一般形式 为:new ST {T x|R(x)&&P(x)},其中的R(x)对应集合中x的范围,通常是来自于某个既有集合中的元素,如s.has(x),P(x)对应x取值的约束。
      • 操作符:
        • 子类型关系操作符: E1<:E2 ,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假。如果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 ==> b_expr2 而言,当b_expr1 == false,或者 b_expr1 == true 且 b_expr2 == true 时,整个表达式的值为true。
        • 变量引用操作符:除了可以直接引用Java代码或者JML规格中定义的变量外,JML还提供了几个概括性的关键词来引用相关的变量。\nothing指示一个空集;\everything指示一个全集,即包括当前作用域下能够访问到的所有变量。
    • (3)方法规格:

      • 前置条件:前置条件通过requires子句来表示: requires P;。其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。
      • 后置条件:后置条件通过ensures子句来表示: ensures P;。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。
      • 副作用范围限定:副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。
    • (4)类型规格:

      类型规格指针对Java程序中定义的数据类型所设计的限制规则,一般而言,就是指针对类或接口所设计 的约束规则。

      • 不变式invariant :
        • 不变式(invariant)是要求在所有可见状态下都必须满足的特性,语法上定义 invariant P ,其中 invariant 为关键词, P 为谓词。
      • 状态变化约束constraint:
        • 状态变化约束(constraint)是对前序可见状态和当前可见状态的关系进行约束。
      • 方法与类型规格的关系:详细解释可见上文所说的JML Level 0手册

2.应用工具链

  • 相关工具可以参考:http://www.eecs.ucf.edu/~leavens/JML//download.shtml。
    • AspectJML tool :AspectJML工具能够为Java和AspectJ程序指定并执行运行时断言检查。
    • jml4c tool :jml4c工具是基于Eclipse Java编译器构建的JML编译器。
    • JMLEclipse:JML Eclipse是在Eclipse的JDT编译器基础设施之上开发的JML工具套件的pre-alpha版本。
    • JMLUnitNG:JMLUnitNG是一个自动生成带有JML注释的Java代码的单元测试工具。
    • JMLOK :JMLOK是一个使用随机测试来对照JML规范检查Java代码,并为发现的问题提出可能的原因的工具。
  • 本人在本单元主要采用了openjml和JMLUnitNG两个工具。在课程实验中编写规格中可以通过openjml检验格式是否正确,在课程作业中可以通过JMLUnitNG生成测试用例和数据。

二、使用SMT Solver进行验证

  • SMT,全称Satisfiability modulo theories,是验证一阶逻辑具有相等性逻辑公式的决策问题。而这里我们可以用SMT Solver来验证我们实现的程序和JML规格的功能是否等价。

  • z3 solver是Microsoft Research开发的一种较为高效的SMT Solver库,同时又由于openJML本身支持z3 solver进行验证,所以我这里直接采用openJML自带的z3 solver程序进行验证。

  • 在使用openJML确定自己的jml规格没有语法错误后(具体方法在下一部分),可通过如下指令使用openJML和附带的z3 solver验证自己实现的代码。

    java -jar openjml\openjml.jar -exec openjml\Solvers-windows\z3-4.7.1.exe -esc test\MyGroup.java test\Person.java
    

    如果验证结果正确,应得到如下结果。(那个警告大家无视就好,这个主要是因为solver不让初始化为null)

    z3solver无输出测试结果

  • 通过添加参数 -verboseness=3 后,我们可以得到验证过程的全部输出: output.rar

    我们可以从这个文件中看到z3 solver对MyGroup类的全部方法的验证过程,但是由于验证过程输出过长,这里只展示三个主要方法的部分验证过程。

    • 构造方法MyGroup()

      TRANSFORMATION OF test.MyGroup.MyGroup(int)
      {
        java.lang.Exception `exception = null;
        int `terminationPosition = 0;
        int __JML_AssumeCheck_;
        // Declaration of THIS
        test.MyGroup `THIS;
        /*@ assume `THIS != null;*/
        /*@ assume `THIS == null || `THIS instanceof test.MyGroup && (\typeof(`THIS) == \type(test.MyGroup) && <:(\typeof(`THIS), \type(test.MyGroup)));*/
        /*@ assume `THIS.`alloc__ == 1;*/
        // Declare formals
        int id1;
        // Declare result of constructor
        test.MyGroup `result = `THIS;
        // Heap value and allocation fields
        {
          // Invariants for discovered fields
          /*missing*/
          /*missing*/
          /*missing*/
        }
        // Assume axioms
        // axiom (\forall \bigint m, \bigint n; m > n && n >= 0; pow256(m) > pow256(n)); 
      
    • addPerson()

      TRANSFORMATION OF test.MyGroup.addPerson(test.Person)
      {
        java.lang.Exception `exception = null;
        int `terminationPosition = 0;
        int __JML_AssumeCheck_;
        // Declaration of THIS
        test.MyGroup `THIS;
        /*@ assume `THIS != null;*/
        /*@ assume `THIS == null || `THIS instanceof test.MyGroup && <:(\typeof(`THIS), \type(test.MyGroup));*/
        /*@ assume `THIS.`alloc__ == 0;*/
        // Declare formals
        /*@ non_null */ 
        test.Person person;
            // Heap value and allocation fields
        {
          // Invariants for discovered fields
          /*missing*/
          /*missing*/
          /*missing*/
          {
            /*@ assume key != null;*/
            /*@ assume (key == null || key instanceof java.lang.Object && <:(\typeof(key), \type(java.lang.Object))) && (key == null || key instanceof java.lang.Comparable && <:(\typeof(key), \type(java.lang.Comparable))) && (key == null || key instanceof java.io.Serializable && <:(\typeof(key), \type(java.io.Serializable))) && (key == null || key instanceof java.lang.Number && <:(\typeof(key), \type(java.lang.Number))) && (key == null || key instanceof java.lang.Integer && <:(\typeof(key), \type(java.lang.Integer)));*/
            // assume ImplicitAssume (key == null || key instanceof java.lang.Object && <:(\typeof(key), \type(java.lang.Object))) && (key == null || key instanceof java.lang.Comparable && <:(\typeof(key), \type(java.lang.Comparable))) && (key == null || key instanceof java.io.Serializable && <:(\typeof(key), \type(java.io.Serializable))) && (key == null || key instanceof java.lang.Number && <:(\typeof(key), \type(java.lang.Number))) && (key == null || key instanceof java.lang.Integer && <:(\typeof(key), \type(java.lang.Integer))); ...
          }
      
    • hasPerson()

      TRANSFORMATION OF test.MyGroup.hasPerson(test.Person)
      {
        java.lang.Exception `exception = null;
        int `terminationPosition = 0;
        int __JML_AssumeCheck_;
        // Declaration of THIS
        test.MyGroup `THIS;
        /*@ assume `THIS != null;*/
        /*@ assume `THIS == null || `THIS instanceof test.MyGroup && (\typeof(`THIS) == \type(test.MyGroup) && <:(\typeof(`THIS), \type(test.MyGroup)));*/
        /*@ assume `THIS.`alloc__ == 1;*/
        // Declare formals
        int id1;
        // Declare result of constructor
        test.MyGroup `result = `THIS;
        // Heap value and allocation fields
        {
          // Invariants for discovered fields
          /*missing*/
          /*missing*/
          /*missing*/
        }
          {
            /*@ assume temPerson != null;*/
            /*@ assume (temPerson == null || temPerson instanceof java.lang.Object && <:(\typeof(temPerson), \type(java.lang.Object))) && (temPerson == null || temPerson instanceof test.Person && <:(\typeof(temPerson), \type(test.Person)));*/
            // assume ImplicitAssume (temPerson == null || temPerson instanceof java.lang.Object && <:(\typeof(temPerson), \type(java.lang.Object))) && (temPerson == null || temPerson instanceof test.Person && <:(\typeof(temPerson), \type(test.Person))); ...
          }
          {
            /*@ assume -2147483648 <= singleVar && singleVar <= 2147483647;*/
            // assume ImplicitAssume -2147483648 <= singleVar && singleVar <= 2147483647;
          }
      
  • 以上这三个函数的验证过程其实还有很多,但实在限于篇幅,所以只能截取部分。但是我们可以从中看到一些共性的验证内容:

    • 每次验证都是首先转化实现部分的代码含义,将其转化为标准的JML格式
    • 通过测试程序和assert验证二者是否等价

三、使用JMLUnitNG进行测试

注意在使用JMLUnitNG进行测试前,首先需要使用openJML检测程序的JML语法是否正确。

  • openJML

    • 对于openJML,个人认为目前较为简便的使用方式是通过命令行,其他比如使用IDEA的External tools或者eclipse的插件的使用方式,本人在使用的过程中产生了各种奇奇怪怪的问题,并且很难解决。

    • 通过openJML检查MyGroup类的jml语法并且修改jml错误后,本人最终得到完全正确的MyGroup类JML如下:

    • import java.math.BigInteger;
      import java.util.HashMap;
      
      public class MyGroup {
      
          /*@ public instance model non_null int id;
            @ public instance model non_null Person[] people;
            @ public instance model non_null int relationSum;
            @ public instance model non_null int valueSum;
            @*/
          private int id1;
          private HashMap<Integer,Person> people1;
      
          public MyGroup(int id1) {
              this.id1 = id1;
              people1 = new HashMap<Integer, Person>();
          }
      
          //@ ensures \result == id;
          public /*@ pure @*/ int getId() {
          }
      
          /*@ also
            @ public normal_behavior
            @ requires obj != null && obj instanceof MyGroup;
            @ assignable \nothing;
            @ ensures \result == (((MyGroup) obj).getId() == id);
            @ also
            @ public normal_behavior
            @ requires obj == null || !(obj instanceof MyGroup);
            @ assignable \nothing;
            @ ensures \result == false;
            @*/
          public boolean equals(Object obj) {
          }
      
          /*@ public normal_behavior
            @ assignable people;
            @ ensures people.length == \old(people.length) + 1;
            @ ensures (\forall int i; 0 <= i && i < \old(people.length);
            @         (\exists int j; 0 <= j && j < people.length;
            @         people[j] == \old(people[i])));
            @ ensures (\exists int i; 0 <= i && i < people.length; people[i] == person);
            @*/
          public void addPerson(Person person) {
          }
      
          //@ ensures \result == (\exists int i; 0 <= i && i < people.length; people[i].equals(person));
          public boolean hasPerson(Person person) {
          }
      
          /*@ 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 int getRelationSum() {
          }
      
          /*@ 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 int getValueSum() {
          }
      
          /*@ 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 BigInteger getConflictSum() {
          }
      
          /*@ public normal_behavior
            @ requires people.length == 0;
            @ ensures \result == 0;
            @ also
            @ public normal_behavior
            @ requires people.length != 0;
            @ ensures \result == ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length);
            @*/
          public /*@ pure @*/ int getAgeMean() {
          }
      
          /*@ public normal_behavior
            @ requires people.length == 0;
            @ ensures \result == 0;
            @ also
            @ public normal_behavior
            @ requires people.length != 0;
            @ ensures \result == ((\sum int i; 0 <= i && i < people.length;
            @          (people[i].getAge() - getAgeMean()) * (people[i].getAge() - getAgeMean())) /
            @           people.length);
            @*/
          public int getAgeVar() {
          }
      
          /*@ public normal_behavior
            @ assignable people;
            @ ensures people.length == \old(people.length) - 1;
            @ ensures (\forall int i; 0 <= i && i < people.length;
            @         (\exists int j; 0 <= j && j < \old(people.length);
            @         people[i] == \old(people[j])));
            @ ensures (\exists int i; 0 <= i && i < \old(people.length); \old(people[i]) == person);
            @*/
          public void delPerson(Person person) {
          }
      
          /*@ public normal_behavior
            @ assignable relationSum, valueSum;
            @ ensures relationSum == \old(relationSum) + 2;
            @ ensures valueSum == \old(valueSum) + 2 * value;
            @*/
          public void update(int value) {
          }
      
          //@ ensures \result == people.length;
          public int getPeopleSum() {
          }
      
      }
      
    • 要对此JML代码使用openJML检查语法,则执行指令:

      java -jar openjml\openjml.jar -check -dir test\MyGroup.java test\Person.java
      

      若得到下图结果,则说明语法正确。

      openJML正确图片

      若语法存在任何问题,将得到类似下图的结果。

      openJML错误图片

    • 而本人在使用openJML的过程中,还遇到了以下几类比较棘手的问题,想在这里给大家分享一下:

      • 一定不要在中文路径下使用openJML,否则会导致openJML发生异常。
      • 由于openJML的检查语法较为严格,所以注意规格中的变量和实现的变量不能重名。
      • openJML不能识别三目表达式,所以注意应该把原有规格中的三目表达式改成相应的requires和ensures的组合。
      • 把需要检查的代码名称在cmd的指令中写全。如果一个类中使用了另外一个类,但是这个类并未写进指令中,将会报错:找不到符号。
      • 在JML规格中使用的方法最好都要提前写好pure,否则它会有警告。(当然无视这一点也可以
  • JMLUnitNG

    • 由于之前已经使用openJML检查过Group类的JML语法,所以接下来如果想使用JMLUnitNG进行测试,需要修改的部分就简单了许多,只需注意如下问题需要修改。

      • 一定要在ArrayList、Hashmap等容器类实例化的过程中写好存储的数据类型,不能使用泛型类,JMLUnitNG会把这个问题视为错误。
    • 接下来分别执行以下三条指令来运行JMLUnitNG进行测试

      java -jar jmlunitng.jar jmlunitngTest\MyGroup.java
      
      javac -cp jmlunitng.jar jmlunitngTest\*.java
      
      java -cp jmlunitng.jar jmlunitngTest.MyGroup_JML_Test
      

      我这里分别解释一下每条指令的作用:

      • 第一条指令,用于生成MyGroup类的测试类,运行完成后,将生成以下测试数据类
        • jmlunitng第一步2
        • 可以看到,JMLUnitNG针对MyGroup的每一个方法都生成了一个测试数据类
      • 第二条指令,用于编译所有的java文件,生成可执行的class文件
      • 第三条指令,用于运行测试,通过运行新生成的MyGroup_JML_Test的class文件来进行测试
    • 最终得到如下运行结果

      • jmlunitng完成图片2
    • 测试分析

      • 通过上图可以看到,JMLUnitNG的测试用例,大多是针对MyGroup类的方法的边界条件。
      • 比如MyGroup类的构造方法中参数是int类型,那么JMLUnitNG将测试参数为2147483647和-2147483648的int类型边界的情况;再比如addPerson这个方法的参数是Person类的对象,那么JMLUnitNG将测试参数为null的边界的情况。这里由于null的异常情况是在MyNetwork类里面才会处理,所以测试结果得到的是Failed。其余方法的测试用例这里不再赘述。

四、作业架构设计

  • 第一次作业

    • 作业类图如下

      第一次作业类图

    • 架构设计

      • 由于第一次作业并未对程序复杂度有较为严格的要求,所以我直接按照规格,大多数容器直接采用了ArrayList。
      • MyPerson类实现了Person类,需要用于管理每一个人的属性以及相互认识的人,以及进行相关查询操作。
      • MyNetwork实现了Network类,用于管理所有的人,进行添加人和查询人之间的关系等操作。
    • 设计难点

      • 本次作业唯一较难的方法是isCircle方法,需要实现判断两人之间是否可达。我采用了并查集算法,通过在MyPerson类添加RootId属性,可实现并查集判断可达性。
  • 第二次作业

    • 作业类图如下

      第二次作业类图

    • 架构设计

      • 由于本次作业数据加强了许多,需要考虑很多查询操作的复杂度,不能直接按照规格直接使用ArrayList进行数据存储,因此,我没有再复用第一次作业的代码,而是重写了全部的代码。
      • MyPerson类中虽没有新的要求,但是需要使用Hashmap来存储相邻的人和距离数据,这样才能保证代码实现后的时间复杂度满足要求。
      • 本次作业新加的类MyGroup,实现将network中的人分组,并查询组内人的相关数据情况。同样采用Hashmap来存储组内人员。同时由于MyGroup的许多查询工作复杂度并不低,且查询操作的指令数量远远大于加入人指令的数量,我采用了存储查询工作结果并在addPerson向组内加人时才更新存储结果的方法,由此可以实时维护查询工作的结果,让查询工作有了O(1)的复杂度。
    • 设计难点

      • MyGroup中查询结果的维护是本次作业的难点,需要注意:不仅是在addPerson将人加入组内的过程会更新查询结果,addRelation向人之间添加关系的过程同样需要更新查询结果。
  • 第三次作业

    • 作业类图如下

      第三次作业类图

    • 架构设计

      • 本次作业的数据量和第二次作业相似,所以仍需考虑设计过程中的复杂度。
      • MyNetwork类中添加了许多新的查询操作,如查询两人之间最短路、查询两人之间是否有两条及以上能相互到达的路径等等。其余属性的添加还是通过Hashmap进行处理。
    • 设计难点

      • 本次作业难点在于上文所说的两个新加的查询操作的设计。我这次分别采用了堆优化Dijkstra和tarjan算法求点双连通分量解决的这两个查询操作。由于堆优化Dijkstra算法总体时间复杂度为O((n+m)logm),tarjan算法时间复杂度为O(n+m),均在数据可以接受的范围内,所以可以直接在查询操作中使用。
  • 模型构建策略:

    • 本单元作业需要从面向对象的角度看出,network中的人实际上组成了一个图,所以主要模型构建策略就是图模型。
    • 构建出图模型后,我们就可以正常使用图中的各种算法来实现jml规格中的各种方法。

五、作业代码bug分析及修复

  • 对于本单元作业bug,本人均采用了两种方式进行测试:
    • 利用JUnit进行单元测试
      • JUnit是本单元指导书建议的一种测试方式,用于测试单一方法的正确性。同时,由于此测试方式具有显示测试覆盖率的功能,所以更适合于测试具有多分支功能的方法。
      • 但是此方法无法实现对程序的大量随机数据测试,只能用手动生成的简单数据来测试方法功能。
    • 生成随机数据进行对拍
      • 生成随机数据然后对拍是比较常见的方法了。需要注意的是,在使用这种方式测试后,一定要注意程序对边界数据的测试。不然即便强测并未测试出程序的bug,互测中同学构造的边界条件一样会让bug现出原形。
  • 本单元产生bug:
    • 本单元前两次作业由于我都采用了以上两种方式进行测试,同时较为仔细的的测试了边界数据,确保既不会运行时错误也不会发生超时问题,所以前两次作业我并没有产生bug。
    • 但是,对于第三次作业,由于我把重心主要放在优化效率上,在对拍测试中也只是测试了大量的随机数据来保证时间,并没有全面的考虑边界数据的测试,所以在MyGroup的一个边界条件——人数为零时,产生了除零的bug,主要原因如下:
      • 为了提升程序效率,我的Group类采用了存储ageMean等中间变量并在addPerson和delPerson的过程中更新中间变量的方式。但是,我忽略了一种情况:在delPerson的过程中,可能导致Group中的人数peopleSum变为0,此时我的程序会更新ageMean,由于ageMean的计算公式为ageSum/peopleSum,这就导致发生了除零的异常。
      • 修复方式就比较简单了,只需在delPerson的过程中特判一下人数为0的情况,将ageMean等中间变量直接设置为0,而不是通过计算得出,由此即可修复此bug。

六、心得体会

  • 本单元接触的规格化设计语言JML,让我对代码设计和代码实现之间的关系理解的更为透彻。进行规格化设计的工作者并不需要考虑方法的具体实现方式和内容,只需要通过JML规格撰写将方法的功能描述完整;进行代码实现的人员也只需要在读懂JML规格的需求后考虑如何实现,而不需要考虑设计层面的问题。这样就较为巧妙地实现了团队开发过程中地分工问题,提高了开发效率。同时,如果开发过程中出现bug,通过JML的相关工具链,也更容易定位错误出现的原因,也提高了解决bug的效率。

  • 此外,虽然规格化设计并未对代码实现过程进行约束,但是其仍然具有较强的严谨性。相较于带有内在模糊性的自然语言描述,规格化设计逻辑严格,能够更加清楚的表达设计者内心的思想,也具有相应的验证方式。由此可见,在这一方面上,规格化设计具有较为严谨的优势。

  • 总之,在本单元中,通过学习JML这个规格化设计语言,让我更加认识到了面向对象程序编写过程中代码设计这一操作的重要性。正是因为能够有严谨详细的规格设计,我们才能在程序编写和代码实现的过程中具有更高的效率。

posted @ 2020-05-23 14:20  OmedetoHe  阅读(200)  评论(0编辑  收藏  举报