一、关于JML
1、理论基础
JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言(Behavior Interface Specification Language, BISL), 基于Larch方法构建。
JML主要用于两个方面:(1)开展规格化设计 (2) 怎对已有代码实现,书写规格,提高代码可维护性
更多细节可以参考here
2、基本语法
JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。其中行注释的表示方式 为 //@annotation ,块注释的方式为 /* @ annotation @*/,annotaion为注释内容。JML一般位于被注释内容的上方。
JML中一些常见的语法成分:
关键字 | 说明 |
---|---|
normal_behavior | 后面为方法的正常行为 |
exceptional_behavior | 后面为方法的异常行为 |
requires | 前置条件,通常为对输入的限制 |
assignable | 副作用范围限定,列出方法能够修改的类成员属性 |
ensures | 后置条件,通常涉及返回值或操作前后类成员属性 |
signals | 异常描述 |
JML常用的语法:
- /@ pure @ / 表示方法是纯查询方法,执行不会有任何副作用
- \result表达式:表示非void类型的方法执行获得的结果,即返回值。
- \old(expr)表达式:评估expr中的对象在方法执行前后发生变化的情况。
- \forall表达式:全称量词修饰的表达式,表示指定范围内的元素,全部满足相应约束。
- \exist表达式:存在量词修饰的表达式,表示存在某个元素满足相应的约束。
方法规格&类型规格
- 方法规格:包括前置条件、后置条件和副作用约定三个部分。前置条件是对方法输入参数的约束;后置条件是对方法执行结果的限制;副作用是指方法执行过程中对输入对象或this对象进行了修改。一个简单的例子如下,recordCredit方法要求正常输入的int变量z,取值在0到100之间,返回值为credits容器的长度,如果输入不满足要求会产生指定的异常:
public abstract class Student {
/** A specification that can't be satisfied. */
//@ public model non_null int[] credits;
/*@ normal_behavior
@ requires z >=0 && z <= 100;
@ assignable \nothing;
@ ensures \result == credits.length;
@ also
@ exceptional_behavior
@ requires z < 0; @ assignable \nothing;
@ signals_only IllegalArgumentException;
@ also
@ exceptional_behavior
@ requires z > 100;
@ assignable \nothing;
@ signals_only OverFlowException;
@*/
public abstract int recordCredit(int z) throws IllegalArgumentException, OverFlowException; }
- 类型规格: 针对Java程序中定义的数据类型设计的限制规则。下面介绍两种限制规则:不变式限制(invariant)和约束限制(constraints)
不变式(invariant): 要求在所有可见状态下都必须满足的特性,语法为 invariant P, invariant为关键词,P是谓词。
状态变化约束(constraint): 对前序可见状态和当前可见状态的关系进行约束。
参考下面的例子:public class ServiceCounter{ private /*@spec_public@*/ long counter; //@ invariant counter >= 0; //@ constraint counter == \old(counter)+1; }
对ServiceCounter类,要求在可见状态下,counter不能为负数,同时要求每次对counter的修改只能是增加1。
3、应用工具链(及相关使用)
(1) Openjml, SMT Solver
openjml可以对用静态或者运行时的方式对程序的行为进行检查。SMT solver则可以对JML进行理论检查,推导代码的正确性。z3, cvc4等SMT solver可以与OpenJML较好地配合使用。
下载链接, 解压后可以找到相应的jar包和.exe文件
使用方式1:命令行。这里展示windows的使用方式,linux和macos类似。
其中,-esc表示静态检查,-rac表示动态检查,只保留一个。 -cp表示需要添加的依赖(jar包等等)。我的windows10似乎只支持z3-4.7.1版本的solver。java -jar <filepath>\openjml.jar -cp <classpath> -esc|-rac <target java file> -exec <solverpath>\z3-4.7.1.exe
- 使用方式2:eclipse插件。虽然IDEA有能装,但是使用的时候蜜汁bug,就不介绍了。
- 首先,Help->install new software, 安装openjml,链接:http://jmlspecs.sourceforge.net/openjml-updatesite 之后,Window->preference,对openjml的solver进行配置。
- 配置完成后,选定文件,JML->ESC静态检查,RAC动态检查生成.class文件,结果如下:(报了很多的异常,不是很明白,另外跑得很慢很慢)
- 首先,Help->install new software, 安装openjml,链接:http://jmlspecs.sourceforge.net/openjml-updatesite 之后,Window->preference,对openjml的solver进行配置。
(2) JMLUnitNG
JMLUnitNG是基于TestNG的,为JML注释的java文件自动生成单元测试的工具。使用指南
基本使用方法:
java -jar jmlunitng.jar [OPTIONS] path-list
其中,path-list是需要检查的文件,可添加的参数中比较重要的是-cp,与openjml使用类似
本次选取MyPath中的部分方法进行验证(对于一些JML语法不支持,只能验证一些简单的例子)
仅保留了isValid, getNode和size对应的JML
首先用javac进行编译,之后用opjml solver-rac, 使动态检查处于Enable状态,然后进行检验,具体可参考讨论区帖子
得到结果如下图:
二、架构设计
- 第一次的架构完全模仿了指导书,仅实现了(实际有用的)方法,没有特别之处
- 第二次作业与第一次相比,需要增加最短路径算法。为此,我单独准备了Matrix类,用于以变形邻接矩阵形式存储图的连接关系和算法实现,类图见下
第三次作业增加了一些新的图算法和图的存储要求,为此我增加了3个新的似Matrix类,继承MultiMatrix类,用于存不同的图,但在MultiMatrix中同一使用了不拆点的算法。为了完成点的信息抽象,单独建立了Pathdistance类存储不同类型的"距离"值。类图见下
关于图算法,我选择了采用不拆点的方式,重新定义距离,算是一种空间换时间的策略。关于优化,采用了局部更新图结构的策略,每次添加新的路径时,记录下路径中任意两点之间的距离,存入图中,如果出现比原有距离小的情况则认为图结构出现了更新,距离矩阵也需要跑Floyd更新。删除路径同理。这样的算法要求局部最优,即每个path添加进来时,必须保证已经求得任意两点之间距离是最短的。三、bug情况
- 自测不积极,强测两行泪
- 第一次JML作业中,compareTo方法由于疏忽将某一处的返回值-1误写为1,导致大量涉及到路径比较的指令结果出错,boom
后两次作业加强了Junit测试和脚本测试的覆盖程度,并且进行了运行时间的测试,没有出现bug
四、心得体会
个人觉得,尽管JML在理论方面相对完善,在实际应用中却存在着工具不完善、使用不方便的问题。从JML的层次进行检查比代码层次的测试要困难得多,这是可以理解的,但是openjml, JMLUnitNG等工具存在缺陷限制了JML的一些应用。只有对工具进行更新、支持更多的语法,才会有更多的开发者使用JML。