OO_JAVA_JML_单元总结

目录
  • 1.JML语言介绍
    • 1.1 JML概念与理论基础
    • 1.2 JML好处
    • 1.3 JML语法
    • 1.4 JML工具链
  • 2. 使用OpenJML和JMLUnitNG
    • 2.1 使用OpenJML
    • 2.2 使用JMLUnitNG
    • 2.3 分析自动生成的测试数据
  • 3. 部署SMT Solver
  • 4. 架构设计分析
    • 第一次作业
    • 第二次作业
    • 第三次作业
    • 反思
  • 5. bug
    • 5.1 bug分布情况
    • 5.2 测试策略
  • 6.心得体会

1.JML语言介绍

1.1 JML概念与理论基础

The Java Modeling Language (JML) is a specification language for Java programs, using
 Hoare style pre- and postconditions and invariants, that follows the design by contract
 paradigm. Specifications are written as Java annotation comments to the source files,
 which hence can be compiled with any Java compiler.
                                                                              --WiKi

也就是说,JML是一种以Java注释的形式,进行契约式设计的规格语言。具体就是通过在JML中写明前置条件,后置条件,不变式等等,使得代码实现者在实现代码时只需要满足JML规格;代码调用者只需理解JMl而不需要看代码的具体实现就能知道类和方法的功能和使用条件。

1.2 JML好处

  • 能防止代码实现者对模型设计者的意图有歧义
  • JML使测试更容易
  • 代码调用者能快速了解类和方法的使用条件的功能

1.3 JML语法

这里仅介绍这一单元常用的语法。

JML结构 功能
requires 定义方法的前置条件
assignable 副作用范围限定,列出这个方法能修改的成员变量
ensures 定义方法的后置条件
public normal_behavoir 表示对方法的正常功能给出规格
public exceptional_behavior 表示对方法的异常功能给出规格
also 表示(1)子类重写父类方法时,补充新的规格 (2)一个方法规格中设计多个方法规格描述
signals (Exception e) expr 表示expr为true时,抛出相应异常 Exceptin e
signals_only Exception 只要满足前置条件就抛出异常
invariant 要求在所有可见状态下都要满足的条件
constraint 对象的状态在发生变化时需要满足的约束
JML原子表达式 功能
\result 表示方法执行后的返回值
\old(exp) 表示一个表达式exp在方法执行前的取值
JML量化表达式 功能
\forall 表示对于给定范围的元素全都满足约束条件
\exists 表示对于给定范围存,在元素满足约束条件
\sum 返回给定范围内的所有表达式的和
\min 返回给定范围内的表达式的最小值
\max 返回给定范围内的表达式的最大值

1.4 JML工具链

JML工具链完整清单
其实在使用过程中,个人感觉JML工具链还远远不够完善,而且近些年好像发展停滞了

2. 使用OpenJML和JMLUnitNG

2.1 使用OpenJML

从OpenJML官网下载OpenJML。
环境: Windows10,z3-4.7.1.exe,jdk 1.8
对第三次作业所有源文件进行分析
使用命令

java -jar  openjml.jar -exec Solvers-windows\z3-4.7.1.exe -esc -dir src

结果如下(部分截图)

    src\com\oocourse\spec3\main\Network.java:58: 注: Not implemented for static checking: ensures clause containing \not_assigned
      @     \old(people[i].getId()) != id2; \not_assigned(people[i]));
                                                         ^
src\com\oocourse\spec3\main\Network.java:183: 注: Not implemented for static checking: ensures clause containing \not_assigned
      @ ensures (\forall int i; 0 <= i < groups.length; \not_assigned(groups[i]));
                                                                     ^
src\com\oocourse\spec3\main\Network.java:275: 注: Not implemented for static checking: ensures clause containing \not_assigned
      @ ensures (\forall int i; 0 <= i < groups.length; \not_assigned(groups[i]));


src\hwthree\MyNetwork.java:66: 警告: NOT IMPLEMENTED: Not yet supported feature in converting BasicPrograms to SMTLIB: JML Quantified expression using \sum
     * @param id2 id2
         ^
错误: An error while executing a proof script for queryGroupAgeMean: (error "Error writing to Z3 solver: java.io.IOException: 管道正在被关闭。")
src\hwthree\MyNetwork.java:74: 警告: NOT IMPLEMENTED: Not yet supported feature in converting BasicPrograms to SMTLIB: JML Quantified expression using \sum
        if (id1 == id2 && contains(id1)) { return; }

2.2 使用JMLUnitNG

step 0 修改文件树
jmlUnitNG运行起来容易报错,为了不报错,对文件树进行修改。

  • 只保留各异常类,Person,Group,MyPerson,Mygroup类
  • 将剩余的所有文件都放在test包目录下,相应修改源代码里面的package和import
  • 将泛型类中的缺省补充完整

OO_JAVA_JML_单元总结_第1张图片

step 1 生成Group的测试文件

java -jar jmlunitng.jar test/MyGroup.java

OO_JAVA_JML_单元总结_第2张图片

step 2 编译测试文件

javac -cp jmlunitng.jar test/*.java

step 3 运行测试文件

java -cp jmlunitng.jar test.MyGroup_JML_Test

运行结果如下:

[TestNG] Running:
  Command line suite

Failed: racEnabled()
Passed: constructor MyGroup(-2147483648)
Passed: constructor MyGroup(0)
Passed: constructor MyGroup(2147483647)
Failed: <>.addPerson(null)
Failed: <>.addPerson(null)
Failed: <>.addPerson(null)
Failed: <>.delPerson(null)
Failed: <>.delPerson(null)
Failed: <>.delPerson(null)
Passed: <>.equals(null)
Passed: <>.equals(null)
Passed: <>.equals(null)
Passed: <>.equals(java.lang.Object@29444d75)
Passed: <>.equals(java.lang.Object@1517365b)
Passed: <>.equals(java.lang.Object@44e81672)
Passed: <>.getAgeMean()
Passed: <>.getAgeMean()
Passed: <>.getAgeMean()
Passed: <>.getAgeVar()
Passed: <>.getAgeVar()
Passed: <>.getAgeVar()
Passed: <>.getConflictSum()
Passed: <>.getConflictSum()
Passed: <>.getConflictSum()
Passed: <>.getId()
Passed: <>.getId()
Passed: <>.getId()
Passed: <>.getPeopleSum()
Passed: <>.getPeopleSum()
Passed: <>.getPeopleSum()
Passed: <>.getRelationSum()
Passed: <>.getRelationSum()
Passed: <>.getRelationSum()
Passed: <>.getValueSum()
Passed: <>.getValueSum()
Passed: <>.getValueSum()
Passed: <>.hasPerson(null)
Passed: <>.hasPerson(null)
Passed: <>.hasPerson(null)

===============================================
Command line suite
Total tests run: 40, Failures: 7, Skips: 0
===============================================

2.3 分析自动生成的测试数据

观察测试数据,发现大概可以分为三类

  • 空指针。值得一提的是,这类测试数据fail的原因是输入没有满足前置条件,因此不是代码的锅
  • 极端数据,INT_MAX,INT_MIN
  • 调用空参数的方法

总体来说,自动生成的测试数据的测试强度很弱,仅能检查一下对空指针和INT_MAX,INT_MIN的处理,难以发现代码的逻辑错误。
因此,我们还是需要自己精心构造datamaker和judger来做测试。

3. 部署SMT Solver

// TODO

4. 架构设计分析

第一次作业

本单元第一次难度低,对性能也没有要求,因此直接对JML规格无脑翻译。

唯一稍有难度的方法:isCircle。使用的是BFS。然而由于第一次作业无脑翻译,没有实现类似邻接链表的结构来表示图的连接关系,因此采用数组遍历(邻接矩阵)的方法实现BFS,复杂度为O(n^2)。

第二次作业

第二次作业,有复杂度限制。
采用遍历容易TLE的方法:

  • relationSum, ValueSum, iscircle O(n^2)
  • AgeMean, AgeVar, GroupConfilctSum, contains, getPerson... O(n)

采用:hash + 邻接链表 + 保存中间结果 的方法

  • hashMap, hashMap :将contains,getPerson等方法的复杂度降为O(1)
  • 缓存ageSum, ageSum2, COnflictSum. 构建复杂度为O(n), 查询复杂度为O(1)
  • 保存邻接链表。isCircle降低为O(V+E)

第三次作业

在第二次作业的基础上,第三次作业比较难的方法有BlockSum, StrongLinked, MinPath.

  • BlockSum其实就是强连通分量个数。采用并查集构建复杂度为O(logn),查询复杂度为O(1)。BFS复杂度为O(V+E)。两者都不会超时。

  • MinPath采用堆优化的dijkstra。需要注意的是,在每次更新当前距离时,是直接将新结点加入堆,但是不会删除相对应的旧节点。因此取到堆顶节点时,需要判断对应的顶点是否已经找到最短路径,如果是,则跳过本次循环。时间复杂度为O((V+E)logE).

  • StrongLinked就是判断是否点双连通。采用 判断连通分量中是否存在割点的方法。为了简单起见,直接对联通分量中除了两端外所有的点枚举判断。复杂度为O(V(V+E))

另外,为了实现架构和算法分离,将BFS,dijkstra等图算法封装到Graphic类。具体UML如下:
OO_JAVA_JML_单元总结_第3张图片

反思

在本单元作业中,并查集显然比BFS效率要高很多。在第二次和第三次作业中,我均没有采用并查集。原因如下:

在第二次作业时,考虑到第三次作业可能会出现删除关系的需求,就没有采用并查集。到了第三次作业,发现实际没有这种需求,但是由于懒得增加新方法,就直接将第二次的BFS重用。

实际上,我的这种思维方式没有遵循模块化(高内聚,低耦合)的设计原则。本次作业中,算法与架构关联不大,是完全可以单独封装的(事实我也是这么做的)。因此,在不改变架构的情况下,多种算法应该是可以灵活替换的。

因此,正确的设计思路应该是:在第二次作业,并查集效率比BFS高,因此采用并查集。但考虑到第三次迭代时可能增加删除关系的需求,因此将算法单独封装一个类。如果第三次作业不能采用并查集,将并查集替换为BFS即可。

5. bug

5.1 bug分布情况

三次作业在中测,强测,互测中均未出现bug。

5.2 测试策略

我觉得我三次作业均无bug(包括前两单元也很少有bug)的原因是我的测试策略。

  • 阅读指导书,手动构造数据测试
  • 小黄鸭测试法,阅读JML和代码。
  • Python实现datamaker和judger。实现测评机。

当然,我的测试策略也有不足之处:没有采用课程组推荐的Junit进行单元测试。

其实使用过,但是感觉Junit构造数据和用assert检查结果过于麻烦,直接实现Python测评机更加方便。而且当数据量很大,Python测评机应该也可以保证逻辑全覆盖。

6.心得体会

第三单元主要是JML规格化设计,让我们提前体验了一把工程开发中的契约式设计,味道极佳。

实现者需要明白,代码实现需要严格满足规格,但是不能对规格照猫画虎。这也正是这一单元的一个难点,需要满足规格的同时保证算法的高效性。

另外,正如在上面4.4提到的,既要保证算法的高效性,又要考虑算法的可扩展性。这就要求将算法尽可能与架构分离,使算法具有可替换性。

再次感谢OO课程组老师和助教大大们,OO课程体验极佳!

你可能感兴趣的:(OO_JAVA_JML_单元总结)