Ohmr的JML作业

一、关于JML语言


1.1、JML概述

JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言(Behavior Interface Specification Language,BISL),基于Larch方法构建。BISL提供了对方法和类型的规格定义手段。所谓接口即一个方法或类型外部可见的内容。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。

一般而言,JML有两种主要的用法:

(1)开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。

(2)针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。

 

1.2、JML基本语法

1、原子表达式

\result : 方法执行的返回值

\old(expr) : 指expr在方法执行前的取值

\not_modify(x, y, ...) : 括号中得到变量在方法执行期间的取值未发生变化

\nonnullelements(container) : container对象存储的对象不会有null

 

2、量化表达式

\forall : 全称量词修饰的表达式。对于给定范围的元素,每个元素都满足约束条件,样例(\forall int i;  0 < i && i < 10; a[i] > 0)

\exists : 存在量词修饰的表达式。对于给定范围的元素,存在一个元素都满足约束条件,样例(\exists int i;  0 < i && i < 10; a[i] > 0)

\sum : 给定范围内表达式的和,样例(\sum int i; i < 5 && i >= 0; i * i);结果为24

\max : 给定范围内表达式最大值,样例(\max int i; i < 5 && i >= 0; i * i);结果为16

\min : 给定范围内表达式最小值,样例(\min int i; i < 5 && i >= 0; i * i);结果为0

 

3、操作符

E1<:E2 : 若E1是E2的子类型,该表达式为真

a_expr <==> b_expr : 其中a_expr与b_expr均为布尔表达式,意味a_expr == b_expr

a_expr <=!=> b_expr : 其中a_expr与b_expr均为布尔表达式,意味a_expr != b_expr

a_expr ==> b_expr : 其中a_expr与b_expr均为布尔表达式,当a_expr == false或a_expr == true && b_expr == true时,该表达式值为true

 

4、方法规格

前置条件:前置条件通过requires子句来表示,例如requires P 。其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。

后置条件:后置条件通过ensures子句来表示: ensures P。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。

副作用范围限定:副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法规格的角度,必须要明确给出副作用范围。JML提供了副作用约束子句,使用关键词 assignable 或者modifiable 。

 

1.3、JML工具链

OpenJML:检查JML语法逻辑

JMLUnit/JMLUnitNG:根据JML自动生成测试程序

 

二、部署SMT Solver


通过github获取最新的OpenJML包后,使用openjml进行试验

Windows PowerShell指令为:

java -jar path\to\openjml.jar -check path\to\myJavaProject\classname.java

 

待测试原码共有三个方法:add(int, int)、sub(int, int)、mult(int, int)

public class Demo {
    //@ ensures \result == a+b;
    public int add(int a, int b){
        return a + b;
    }

    //@ requires a > 0;
    //@ requires b > 0;
    //@ ensures \result == a-b;
    public int sub(int a, int b){
        return a - b;
    }

    //@ requires a > 0;
    //@ requires b > 0;
    //@ ensures \result == a*b;
    public int mult(int a, int b){
        return a * b;
    }
}

执行指令后后未报错,指令如下:

将JML中的\result修改为result后,报错如下:

提示未定义变量result,需要改为\result

 

三、部署JMLUnit


从讨论区下载JMLUnit.jar包后,在Windows PowerShell中执行指令

java -jar path\to\jmlunitng.jar path\to\Myproject\classname.java

依旧针对上述代码进行试验,实验前文件树如下:

执行指令

执行指令后文件树如下:

运行Demo_JML_TEST.java进行测试,控制台输出结果如下:

 

四、梳理架构与bug分析


 本节作业是本学期“面向对象”三次试验中最不理想的一次,虽然JML概念理解未出现问题,但是评测的点均为数据结构的选择和性能上,在未经说明的情况下出现了很多CTLE问题。

在此提出一个小小的建议,与其花大工夫让大家尝试使用JMLUnit、OpenJML,不如普及一下Java Cpu时间的测试方法,代码正确性很容易保证,但是性能则不一定。在寻求周边同学帮助后,依然无法获取CPU运行时间,那便很难从根本上实现性能测试。同时发现,本地测试效果与评测机运行时间差异较大,猜测可能是因为运行环境、硬件性能的差异,如此,本地针对性能的测试几乎没有参考价值可言,然而本单元最大的问题是性能不足而非正确率无法保证,我相信这样的情况一定背离了面向对象这门课的初心。

 

第一次作业:

要求实现MyPath、MyPathContainer类,其中MyPathContainer中需要存储所有添加的Path。

在提交的作业版本中,选择使用ArrayList作为存储结构,性能出现了很严重的CPU超时。

解决方法,选用HashMap作为存储结构,在MyPathContainer中维护两个HashMap,实现PathId与Path之间的双向索引。同时记录distinctnum,除非图结构发生变化,否则直接返回上次计算结果。

 

第二次作业:

要求实现MyPath、MyGraph类,其中MyGraph中除了PathContainer,还需实现最短路径索引。

在这一部分,需要记录每次最短路径的结果,之后若再次索引同一最短路径,可以直接通过索引结果获得。也就是通过空间换时间。在第一次作业的基础上,添加了图结构的计算,原先第一次作业的内容并未有所修改。

这里我的程序出现了一个比较意外的错误导致性能不佳,笔者重写的Path类的HashCode方法,将所有Path的hash值设定为1,这样可以保证对于满足equal关系的Path,都可以通过HashMap索引到同一个value。然而在笔者阅读了HashMap的方法后,发现所有的key若含有相同的Hash值,将会严重影响性能,故最终将其修改为当且仅当两个Path满足equal关系时才会拥有相同的Hash值。

 

第三次作业:

这一部分作业与第二次作业大同小异,需要维护更多的图结构,同时需要记录的信息也远多于第二次。这里对第二次作业进行了一些重构,内容如下:

  • HashMap的key不再是int类型的pathId,而是String类型
  • 当执行图结构变更指令,入addPath,removePath等,不再从0开始搭建现有图结构,而是在原先图结构的基础上,增删相应的路径

 

五、心得体会


1、这一单元接触最多的是Java各类数据结构的使用,当早期的万金油ArrayList无法满足需求时,HashMap、HashSet一众数据结构便站了出来。同时通读了HashMap代码之后,让笔者深刻了解到切不能随性重载函数,否则虽然正确性能得到保障,性能却会受到严重影响。

2、同时为了提升性能,有的时候空间换时间是很有必要的,将当前结果通过一个优秀的数据结构记录下来,会为之后的运算带来较为明显的提速。

3、通过本单元,还初步入门了JML,这种代码风格的注释说明能很好的描述需求。在早期学习的时候,会被这种代码风格的注释欺骗,如果严格按照注释格式实现代码,会出现很严重的性能不足问题。所以JML本质上只是个说明,它不提供任何实现上的指导,因此代码实现和JML描述需要划清界限。

posted @ 2019-05-22 17:03  Ohmr  阅读(199)  评论(1编辑  收藏  举报