文章目录
一、梳理JML的理论基础、应用工具链情况。
JML简介
我们首先先来看一下JML的官方定义。
The Java Modeling Language (JML) is a behavioral interface specification language that can be used to specify the behavior of Java modules.
JML(Java Modeling Language)是一种用于规范Java程序行为的行为接口规范语言。JML为方法和类型的规格进行定义,为程序的形式化验证提供了基础,通过工具链可以实现静态检查和自动测试数据生成。
一般而言,JML有两种主要的用法:
- 开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。
- 针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。
就个人理解而言,JML是一种只关注方法和类型的规格定义的手段,也就是只关注一个方法或类型外部可见而忽略内部具体实现过程的内容。
简单来说,编写一个程序可以分为三个部分。
第一步,根据需求进行程序任务的划分。就像荣文戈老师在研讨课上说的那样,你要假装有好几个程序员在同时帮你编写代码,而你现在要做的是,完成需求的划分。就像程序在执行过程中的多线程一样,在编写程序之前,可以通过对任务需求的合理划分形成多个模块化设计,且相互之间没有太多的牵绊因素,这个模块可以是一个类或者是一个方法。在这一阶段我们可以忽略内部具体实现方法,而关注模块的行为接口,也就是模块的外部可见的部分的正确性。我们在以往的程序设计中多半是通过自然语言或者在一些小型程序上直接跳过这一步骤,但是随着今后实际开发中的代码量不断提高,有一种规范化的设计语言,来实现设计和代码编写的分离,可以让程序开发人员和程序测试人员进一步分离,进一步提高效率。
第二步,针对第一步中划分的模块,进行具体的代码编写,在这一阶段不需要考虑各个模块之间的相互调用,而只要根据相应的模块接口的定义,在保证实现需求的正确性的基础上提高代码的运行效率。这一阶段主要考虑的是实现行为接口的数据结构和算法。
第三步,在实现前两步之后,需要对已经编写的代码进行检验和改进。在这个时候,JML不仅可以用来形式化检验程序的正确性以及代码的实现情况,还可以进一步提高程序的可维护性,就像是一本程序的使用说明书。
在程序开发的过程中,我们可以发现JML贯穿了始终,从最早的需求开始到最后的测试都离不开JML的帮助,所以不难看出JML在实际开发中的巨大作用。
JML语法
注释结构
我们首先来看一下JML的注释结构。JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。
- 行注释:
//@annotation
- 块注释:
/*@ annotation @*/
- 按照Javadoc习惯,JML注释一般放在被注释成分的紧邻上部。
表达式
我们接下来看一下JML中使用较多的表达式。
- 原子表达式
\result
:表示一个非void 类型的方法执行所获得的结果,即方法执行后的返回值\old( expr )
:用来表示一个表达式expr
在相应方法执行前的取值。\not_assigned(x,y,...)
表达式:用来表示括号中的变量是否在方法执行过程中被赋值。
- 量化表达式
\forall
表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。\exists
表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。\sum
表达式:返回给定范围内的表达式的和。\max
表达式:返回给定范围内的表达式的最大值。\min
表达式:返回给定范围内的表达式的最小值。
- 集合表达式
- 可以在JML规格中构造一个局部的集合(容器),明确集合中可以包含的元素。
- 操作符
- 子类型关系操作符:
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
。可以看出,这两个操作符和Java中的==和!=具有相同的效果。 - 推理操作符:
b_expr1== >b_expr2
或者b_expr2< ==b_expr1
,对于表达式b_expr1==>b_expr2
而言,当b_expr1==false
,或者b_expr1==true
且b_expr2==true
时,整个表达式的值为true 。 - 变量引用操作符
\nothing
指示一个空集\everything
指示一个全集
- 子类型关系操作符:
方法规格
方法规格的核心内容包括三个方面,前置条件、后置条件和副作用约定。
-
前置条件(pre-condition):前置条件是对方法输入参数的限制,如果不满足前置条件,方法执行结果不可预测,或者说不保证方法执行结果的正确性。
具体的实现形式为通过requires子句来表示:
requires P;
-
后置条件(post-condition):后置条件是对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,否则执行错误。
具体的实现形式为通过ensures子句来表示:
ensures P;
-
副作用范围限定(side-effects):副作用指方法在执行过程中对输入对象或this对象进行了修改(对其成员变量进行了赋值,或者调用其修改方法)。
具体的实现形式为使用关键词
assignable
或者modifiable
。
除了上述三个方面,在JML中还有一些常用的方法规格。
pure
关键词:设计中会出现某些纯粹访问性的方法,即不会对对象的状态进行任何改变,也不需要提供输入参数,这样的方法无需描述前置条件,也不会有任何副作用,且执行一定会正常结束。对于这类方法,可以使用简单的(轻量级)方式来描述其规格,即使用pure
关键词。- 异常处理规格:在JML中,
public normal_behavior
表示接下来的部分对方法的正常功能给出规格。与正常功能相对应的是异常功能,即public exceptional_behavior
。signals
:signals
子句的结构为signals (Exception e) b_expr;
,意思是当b_expr
为true
时,方法会抛出括号中给出的相应异常e
。
类型规格
类型规格指针对Java程序中定义的数据类型所设计的限制规则,一般而言,就是指针对类或接口所设计的约束规则。从面向对象角度来看,类或接口包含数据成员和方法成员的声明及或实现。不失一般性,一个类型的成员要么是静态成员(static member),要么是实例成员(instance member)。一个类的静态方法不可以访问这个类的非静态成员变量(即实例变量)。静态成员可以直接通过类型来引用,而实例成员只能通过类型的实例化对象来引用。因此,在设计和表示类型规格时需要加以区分。
JML针对类型规格定义了多种限制规则,从课程的角度,我们主要涉及两类,不变式限制(invariant)和约束限制(constraints)。
- 不变式invariant:不变式(invariant)是要求在所有可见状态下都必须满足的特性,语法上定义
invariant P
,其中invariant
为关键词,P
为谓词。对于类型规格而言,可见状态(visible state)是一个特别重要的概念。 - 状态变化约束constraint:对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。JML为了简化使用规则,规定invariant只针对可见状态(即当下可见状态)的取值进行约束,而是用constraint来对前序可见状态和当前可见状态的关系进行约束。
JML工具链
与JML规格化设计相关的工具主要有:OpenJML、SMT Solver、JMLUnitNG/JMLUnit等。
- OpenJML:用于检验JML规格的正确性,可以对程序进行动态和静态检查。
- SMT Solver:用于验证代码和规格的等价性。
- JMLUnitNG/JMLUnit:用于JML注释Java代码的自动化单元测试生成工具。
这些工具链的具体使用会在后文中提及。JML完整工具链可以从如下地址获取:http://www.eecs.ucf.edu/~leavens/JML//download.shtml
二、部署SMT Solver,至少选择3个主要方法来尝试进行验证,报告结果有可能要补充JML规格
首次尝试
OpenJML和SMT Solver的下载地址如下:http://www.openjml.org/
首先我在文件夹中直接通过命令行对单个的Group.java文件使用如下命令进行验证:
java -jar D:\jml\openjml\openjml.jar -exec D:\jml\openjml\Solvers-windows\z3-4.7.1.exe -esc D:\jml\test\Group.java -encoding UTF-8
- -exec参数用于指定Solver可执行程序
- -esc参数指定检查类型为Extended Static Checking
- -encoding参数指定编码类型,防止中文编码报错
得到如下结果:
看来不能单独将其中的类复制出来进行验证,我们接下来指定源文件地址再进行尝试。
Parsing and Type-checking
在OpenJML解压缩得到的文件夹中启动cmd,并运行如下命令进行JML的静态语法检查。
java -jar openjml.jar -check "$FilePath$" -cp "$Classpath$" -sourcepath "$Sourcepath$" -encoding utf-8
- check参数指定检查类型为Parsing and Type-checking
- -cp和-sourcepath命令用于指定Classpath和源文件目录
- -encoding参数用于指定文件编码
我的本地我运行的具体指令如下:
java -jar openjml.jar -prover z3_4_7 -exec ".\Solvers-windows\z3-4.7.1.exe" -esc "E:\Code\BUAA-2020-OO\jml\src\com\oocourse\spec3\main\Group.java" -cp "E:\Code\BUAA-2020-OO\jml\out" -sourcepath "E:\Code\BUAA-2020-OO\jml\src" -encoding utf-8
以下是我对第三次作业的MyGroup.java文件进行静态语法检查得到的结果。
Extended Static Checking
接下来利用如下命令Solver对按照JML编写的程序进行简单的静态检查。
java -jar openjml.jar -prover z3_4_7 -exec ".\Solvers-windows\z3-4.7.1.exe" -esc "$FilePath$" -cp "$Classpath$" -sourcepath "$Sourcepath$" -encoding utf-8
- prover参数用于指定Solver类型
- exec参数用于指定Solver可执行程序
- esc参数指定检查类型为Extended Static Checking。
我的本地运行的具体命令如下:
java -jar openjml.jar -prover z3_4_7 -exec ".\Solvers-windows\z3-4.7.1.exe" -esc "E:\Code\BUAA-2020-OO\jml\src\MyPerson.java" -cp "E:\Code\BUAA-2020-OO\jml\out" -sourcepath "E:\Code\BUAA-2020-OO\jml\src" - encoding utf-8
在运行过程中虽然没有产生错误,但一共产生了60个警告。
以下为部分运行结果:
E:\Code\BUAA-2020-OO\jml\src\MyPerson.java:167: 警告: The prover cannot establish an assertion (NullFiel
d) in method MyPerson
private HashSet<Person> linked;
^
E:\Code\BUAA-2020-OO\jml\src\MyPerson.java:168: 警告: The prover cannot establish an assertion (NullFiel
d) in method MyPerson
private Boolean strongLinked;
^
openjml.jar(specs/java/util/Map.jml):184: 警告: The prover cannot establish an assertion (UndefinedCalledMethodPrecondition: openjml.jar(specs/org/jmlspecs/lang/map.jml):18: 注: ) in method addAcquaintance
@ ensures \result == \old(modelMap).get(key);
^
注: Call stack
openjml.jar(specs/java/util/Map.jml):184: 注: : org.jmlspecs.lang.map.get
E:\Code\BUAA-2020-OO\jml\src\MyPerson.java:89: 注: : java.util.HashMap.put
openjml.jar(specs/org/jmlspecs/lang/map.jml):18: 警告: Associated declaration: openjml.jar(specs/java/ut
il/Map.jml):184: 注:
model public function V get(K k);
^
openjml.jar(specs/org/jmlspecs/lang/map.jml):16: 警告: Precondition conjunct is false: containsKey(k)
requires containsKey(k);
^
openjml.jar(specs/java/util/Map.jml):184: 警告: The prover cannot establish an assertion (UndefinedCalle
dMethodPrecondition: openjml.jar(specs/org/jmlspecs/lang/map.jml):18: 注: ) in method addAcquaintance
@ ensures \result == \old(modelMap).get(key);
^
注: Call stack
openjml.jar(specs/java/util/Map.jml):184: 注: : org.jmlspecs.lang.map.get
E:\Code\BUAA-2020-OO\jml\src\MyPerson.java:90: 注: : java.util.HashMap.put
openjml.jar(specs/org/jmlspecs/lang/map.jml):18: 警告: Associated declaration: openjml.jar(specs/java/ut
il/Map.jml):184: 注:
model public function V get(K k);
^
openjml.jar(specs/org/jmlspecs/lang/map.jml):16: 警告: Precondition conjunct is false: containsKey(k)
requires containsKey(k);
^
E:\Code\BUAA-2020-OO\jml\src\MyPerson.java:85: 警告: The prover cannot establish an assertion (Exception
alPostcondition: E:\Code\BUAA-2020-OO\jml\src\com\oocourse\spec3\main\Person.java:66: 注: ) in method co mpareTo
return this.name.compareTo(p2.getName());
^
E:\Code\BUAA-2020-OO\jml\src\com\oocourse\spec3\main\Person.java:66: 警告: Associated declaration: E:\Co
de\BUAA-2020-OO\jml\src\MyPerson.java:85: 注:
@ public normal_behavior
^
E:\Code\BUAA-2020-OO\jml\src\MyPerson.java:85: 警告: The prover cannot establish an assertion (Postcondi
tion: E:\Code\BUAA-2020-OO\jml\src\com\oocourse\spec3\main\Person.java:67: 注: ) in method compareTo
return this.name.compareTo(p2.getName());
^
E:\Code\BUAA-2020-OO\jml\src\com\oocourse\spec3\main\Person.java:67: 警告: Associated declaration: E:\Co
de\BUAA-2020-OO\jml\src\MyPerson.java:85: 注:
@ ensures \result == name.compareTo(p2.getName());
^
openjml.jar(specs/java/lang/String.jml):583: 警告: The prover cannot establish an assertion (UndefinedCa
lledMethodPrecondition: openjml.jar(specs/java/lang/CharSequence.jml):48: 注: ) in method compareTo
@ requires equal(charArray,anotherString.charArray);
^
注: Call stack
openjml.jar(specs/java/lang/String.jml):583: 注: : java.lang.CharSequence.equal
E:\Code\BUAA-2020-OO\jml\src\MyPerson.java:92: 注: : java.lang.String.compareTo
openjml.jar(specs/java/lang/CharSequence.jml):48: 警告: Associated declaration: openjml.jar(specs/java/lang/String.jml):583: 注: public static pure helper model boolean equal(char[] a, char[] b);
^
openjml.jar(specs/java/lang/CharSequence.jml):48: 警告: Precondition conjunct is false: a != null
public static pure helper model boolean equal(char[] a, char[] b);
^
E:\Code\BUAA-2020-OO\jml\src\MyPerson.java:188: 警告: The prover cannot establish an assertion (Possibly BadCast) in method dfs: a com.oocourse.spec3.main.Person cannot be proved to be a MyPerson for (Person person : ((MyPerson) curPerson).getAcquaintance()) {
简单梳理之后,不难发现警告都集中在如下三种:
- The prover cannot establish an assertion
- Precondition conjunct is false
- Associated declaration
由此可见,该功能还不甚完善,距离实际应用还有些距离。
三、部署JMLUnitNG/JMLUnit,针对Group接口的实现自动生成测试用例,并结合规格对生成的测试用例和数据进行简要分析
JMLUnitNG的下载地址如下:http://insttech.secretninjaformalmethods.org/software/jmlunitng/
具体需要依次执行如下命令:
java -jar jmlunitng.jar test/Group.java
javac -cp jmlunitng.jar test/*.java
java -jar openjml.jar -rac test/Group.java test/Person.java
java -cp jmlunitng.jar test.Group_JML_Test
首先我们利用jmlunitng.jar生成测试代码。
从报错中我们可以得知,使用到Map相关容器时需要指明key、value才能进行下一步操作。在对代码进行相应修改后,我们得到如下结果。
[TestNG] Running:
Command line suite
Failed: racEnabled()
Passed: constructor MyGroup(-2147483648)
Passed: constructor MyGroup(0)
Passed: constructor MyGroup(2147483647)
Failed: <MyGroup@8000001f>.addPerson(null)
Failed: <MyGroup@1f>.addPerson(null)
Failed: <MyGroup@8000001e>.addPerson(null)
Passed: <MyGroup@8000001f>.addRelation(-2147483648)
Passed: <MyGroup@1f>.addRelation(-2147483648)
Passed: <MyGroup@8000001e>.addRelation(-2147483648)
Passed: <MyGroup@8000001f>.addRelation(0)
Passed: <MyGroup@1f>.addRelation(0)
Passed: <MyGroup@8000001e>.addRelation(0)
Passed: <MyGroup@8000001f>.addRelation(2147483647)
Passed: <MyGroup@1f>.addRelation(2147483647)
Passed: <MyGroup@8000001e>.addRelation(2147483647)
Failed: <MyGroup@8000001f>.delPerson(null)
Failed: <MyGroup@1f>.delPerson(null)
Failed: <MyGroup@8000001e>.delPerson(null)
Passed: <MyGroup@8000001f>.equals(null)
Passed: <MyGroup@1f>.equals(null)
Passed: <MyGroup@8000001e>.equals(null)
Passed: <MyGroup@8000001f>.equals(java.lang.Object@61baa894)
Passed: <MyGroup@1f>.equals(java.lang.Object@b065c63)
Passed: <MyGroup@8000001e>.equals(java.lang.Object@768debd)
Passed: <MyGroup@8000001f>.getAgeMean()
Passed: <MyGroup@1f>.getAgeMean()
Passed: <MyGroup@8000001e>.getAgeMean()
Passed: <MyGroup@8000001f>.getAgeVar()
Passed: <MyGroup@1f>.getAgeVar()
Passed: <MyGroup@8000001e>.getAgeVar()
Passed: <MyGroup@8000001f>.getConflictSum()
Passed: <MyGroup@1f>.getConflictSum()
Passed: <MyGroup@8000001e>.getConflictSum()
Passed: <MyGroup@8000001f>.getId()
Passed: <MyGroup@1f>.getId()
Passed: <MyGroup@8000001e>.getId()
Passed: <MyGroup@8000001f>.getPeopleSum()
Passed: <MyGroup@1f>.getPeopleSum()
Passed: <MyGroup@8000001e>.getPeopleSum()
Passed: <MyGroup@8000001f>.getRelationSum()
Passed: <MyGroup@1f>.getRelationSum()
Passed: <MyGroup@8000001e>.getRelationSum()
Passed: <MyGroup@8000001f>.getValueSum()
Passed: <MyGroup@1f>.getValueSum()
Passed: <MyGroup@8000001e>.getValueSum()
Passed: <MyGroup@8000001f>.hasPerson(null)
Passed: <MyGroup@1f>.hasPerson(null)
Passed: <MyGroup@8000001e>.hasPerson(null)
Passed: <MyGroup@8000001f>.hashCode()
Passed: <MyGroup@1f>.hashCode()
Passed: <MyGroup@8000001e>.hashCode()
从运行结果我们可以发现,JMLUnitNG在测试过程中主要集中在对边界和类似于null极端情况的测试,而对于一般情况则没有涉及。
四、按照作业梳理自己的架构设计
第一次作业相对来说难度不大,我们不必考虑类之间的相互联系和调用依赖,在大多数情况下只要单独考虑某个类的行为。此外,在第一次作业中,对于数据结构和算法的要求并不是很高。所以,第一次作业主要的目标是理解并熟悉JML相关语法,在理解的基础上实现JML的需求。
在之后两次作业中,我们会逐渐发现,实现相关需求其实是一个很低的标准。在第一次作业中稍微有点理解难度的是MyNetwork中的isCircle方法,简单来说这个函数的目的判断两个人是否联通。对这个方法,我以id1为起点通过dfs深度优先搜索出所有id1的联通节点,然后判断是否包含id2。
在第二次作业中凸显了在实现需求的过程中选择合适的数据结构的重要性。
由于第二次作业中存在大量的查找、添加等操作,所以使用最常规的ArrayList会造成运行时间过长的问题,又由于Person和Group都具有一个唯一的id,所以我们不难想到用id作为key值的HashMap类代替ArrayList以提高程序的运行速度。虽然在一定程序上加大了内存消耗,但是能够有效地提高运行速度。此外,在第二次作业中,我还使用了缓存的方法以提高relationSum和peopleSum的查询,这里要注意缓存何时需要更新的问题。除了addPerson和delPerson方法,当addRelation时也需要对其进行过更新。
在第三次作业中凸显了选择高效的算法的重要性。
这次作业的三个难点就是queryMinPath、queryStrongLinked和queryBlockSum。
- queryMinPath:这个函数
顾名思义可知是最短路径问题,故采用Dijkstra算法来实现,此外还要通过堆优化来降低算法的复杂度。在此次作业中我使用的是Java自带的priorityQueue来实现堆优化。 - queryStrongLinked:这个函数是关于强连通的判断,在本次作业中指的是点双连通,也就是从起点到终点有两条除起点和终点外没有相同节点的路径。我一开始使用的是用dfs判断是否存在一条没有重复节点的环路,但是这个方法存在复杂度过高的问题。因此我之后采用了Tarjan算法求出所有的点双连通分量,然后遍历所有的点双连通分量,判断是都存在一个包含起点和终点的点双连通分量。注意在这里跟传统的点双连通分量有一点不同:在本次作业中,从起点到终点直达的路径不能包含的点双连通分量中。
- queryBlockSum:这个方法求的是连通块的数量,这个方法较前两个方法要简单一些。只要通过dfs或者bfs不断地便利节点,直至所有的节点都被访问过为止。而dfs遍历的次数就是我们要求的连通块的数量。
以下是三个作业的类图。
第一次作业类图
第二次作业类图
第三次作业类图
五、按照作业分析代码实现的bug和修复情况
第一次作业
第一次作业中在一些细节上没有完全按照JML的要求进行编写,导致没能进入互测。
第二次作业
第二次作业中没有产生bug。
第三次作业
第三次作业中我的qsl方法一开始并没有采用Tarjan算法,而是通过dfs寻找一个包含两个点的环路,这导致了复杂度过高的问题。
六、阐述对规格撰写和理解上的心得体会
通过JML等规格化设计我们将设计和实现进行了分离,同时也将代码编写和代码检测进行了分离。这对我们之后实际工作中非常重要,同时我们之前常常忽略的部分。
我个人认为,这个单元的主要目标并不是为了掌握一门JML语言在以后的实际学习工作中使用,而是学习一种思想,一种契约化的思想,一种设计和实现的分离的思想,我觉得这才是我们这个单元学习的重点所在。
、按照作业分析代码实现的bug和修复情况
第一次作业
第一次作业中在一些细节上没有完全按照JML的要求进行编写,导致没能进入互测。
第二次作业
第二次作业中没有产生bug。
第三次作业
第三次作业中我的qsl方法一开始并没有采用Tarjan算法,而是通过dfs寻找一个包含两个点的环路,这导致了复杂度过高的问题。
六、阐述对规格撰写和理解上的心得体会
通过JML等规格化设计我们将设计和实现进行了分离,同时也将代码编写和代码检测进行了分离。这对我们之后实际工作中非常重要,同时我们之前常常忽略的部分。
我个人认为,这个单元的主要目标并不是为了掌握一门JML语言在以后的实际学习工作中使用,而是学习一种思想,一种契约化的思想,一种设计和实现的分离的思想,我觉得这才是我们这个单元学习的重点所在。
不过我们也不难发现,JML的相关工具链还是存在不完善、缺少维护的问题,而就目前看来如此的形式化规格表述也存在着过于繁琐复杂的问题,期待未来能有更加完善的生态环境以及更加优化的语法形式的出现。