- JML理论基础
- 注释结构
- 常用表达式
- 方法规格
- 类型规格
- JML工具链
- SMT Solver
- Parsing and Type-checking
- Extended Static Checking
- Runtime Assertion Checking
- JMLUnitNG
- HW1
- 架构设计1
- Class Diagram1
- 代码规模分析1
- HW2
- 架构设计2
- Class Diagram2
- 代码规模分析2
- HW3
- 架构设计3
- Class Diagram3
- 代码规模分析3
- 总结
- Test n Bugs
- 测试方法
- 测试情况
- 心得体会
- Test n Bugs
JML理论基础
JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言(Behavior Interface Specification Language,BISL),基于Larch方法构建。BISL提供了对方法和类型的规格定义手段。所谓接口即一个方法或类型外部可见的内容。JML主要由Leavens教授在Larch上的工作,并融入了Betrand Meyer, John Guttag等人关于Design by Contract的研究成果。近年来,JML持续受到关注,为严格的程序设计提供了一套行之有效的方法。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。
一般而言,JML有两种主要的用法:
开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。
针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。
注释结构
JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。其中行注释的表示方式为//@annotation
,块注释的方式为/* @ annotation @*/
。按照Javadoc习惯,JML注释一般放在被注释成分的紧邻上部。
常用表达式
\result表达式:表示一个非void
类型的方法执行所获得的结果,即方法执行后的返回值。\result表达式的类型就是方法声明中定义的返回值类型。
\old(expr
)表达式:用来表示一个表达式expr
在相应方法执行前的取值。该表达式涉及到评估expr
中的对象是否发生变化,遵从Java的引用规则,即针对一个对象引用而言,只能判断引用本身是否发生变化,而不能判断引用所指向的对象实体内容是否发生变化。作为一般规则,任何情况下,都应该使用\old把关心的表达式取值整体括起来。
\not_assigned(x,y,...)表达式:用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为true
,否则返回false
。实际上,该表达式主要用于后置条件的约束表示上,即限制一个方法的实现不能对列表中的变量进行赋值。
\forall表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。(\forall int i,j; 0 <= i && i < j && j < 10; a[i] < a[j])
,意思是针对任意0<=i
true
),则表明数组a实际是升序排列的数组。
\exists表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。(\exists int i; 0 <= i && i < 10; a[i] < 0)
,表示针对0<=i<10,至少存在一个a[i]<0。
\sum表达式:返回给定范围内的表达式的和。(\sum int i; 0 <= i && i < 5; i)
,这个表达式的意思计算[0,5)范围内的整数i的和,即0+1+2+3+4==10。注意中间的0 <= i && i < 5
是对i范围的限制,求和表达式为最后面的那个i
。同理,我们构造表达式(\sum int i; 0 <= i && i < 5; i*i)
,则返回的结果为1+4+9+16。
\nothing:表示一个空集,常常在副作用中使用,assignable \nothing
表示这个方法没有副作用。
方法规格
- 前置条件(pre-condition)
前置条件通过requires子句来表示:requires P;
。其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。注意,方法规格中可以有多个requires子句,是并列关系,即调用者必须同时满足所有的并列子句要求。
- 后置条件(post-condition)
后置条件通过ensures子句来表示:ensures P;
。其中ensures是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。同样,方法规格中可以有多个ensures子句,是并列关系,即方法实现者必须同时满足有所并列ensures子句的要求。
- 副作用范围限定(side-effects)
副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法规格的角度,必须要明确给出副作用范围。JML提供了副作用约束子句,使用关键词assignable
或者modifiable
。
- signals子句
signals子句的结构为signals (***Exception e) b_expr;
,意思是当b_expr
为true
时,方法会抛出括号中给出的相应异常e
。还有一个简化的signals子句,即signals_only子句,后面跟着一个异常类型。signals子句强调在对象状态满足某个条件时会抛出符合相应类型的异常;而signals_only则不强调对象状态条件,强调满足前置条件时抛出相应的异常。
为了有效的区分方法的正常功能行为和异常行为,JML提供了这两类行为的区分机制,可以明确按照这两类行为来分别描述方法的规格。其中public normal_behavior
表示正常功能给出规格。所谓正常功能,一般指输入或方法关联this对象的状态在正常范围内时所指向的功能。与正常功能相对应的是异常功能,即public exceptional_behavior
下面所定义的规格。其中的public
指相应的规格在所在包范围内的所有其他规格处都可见。需要说明的是,如果一个方法没有异常处理行为,则无需区分正常功能规格和异常功能规格,因而也就不必使用这两个关键词。作为一种重要的设计检查原则,同一个方法的正常功能前置条件和异常功能前置条件一定不重叠。
类型规格
JML针对类型规格定义了多种限制规则,从课程的角度,我们主要涉及两类,不变式限制(invariant)和约束限制(constraints)。无论哪一种,类型规格都是针对类型中定义的数据成员所定义的限制规则,一旦违反限制规则,就称相应的状态有错。
- 不变式invariant
不变式(invariant)是要求在所有可见状态下都必须满足的特性,语法上定义invariant P
,其中invariant
为关键词,P
为谓词。
- 状态变化约束constraint
对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。JML为了简化使用规则,规定invariant只针对可见状态(即当下可见状态)的取值进行约束,而是用constraint来对前序可见状态和当前可见状态的关系进行约束。
JML工具链
JML的一大优势就在于其丰富的外围工具,具体可见http://www.eecs.ucf.edu/~leavens/JML//download.shtml。其中包括主要的:
- OpenJML:首选的JML相关工具,以提供全面且支持最新Java标准的JML相关支持为目标,能够进行静态规格检查(ESC,Extended Static Cheking)、运行时规格检查(RAC,Runtime Assertion Checking)和形式化验证等一系列功能。OpenJML提供了自带的命令行版本和Eclipse插件版本。
- JML Editing:官方的Eclipse插件,提供了JML规格的代码高亮及代码补全。
- JMLUnitNG:JMLUnit的替代工具,能够根据JML规格自动生成基于测试库TestNG的单元测试集。它是一个用于jml注释的Java代码的自动化单元测试生成工具,包括使用Java 1.5+特性(如泛型、枚举类型和增强的for循环)的代码。与最初的JMLUnit一样,它使用JML断言作为测试预言。它改进了原来的JMLUnit,允许为被测试类的每个方法参数轻松定制数据,以及使用Java反射自动生成非基本类型的测试数据。
- jmldoc:能够通过JML生成javadoc的工具。现已合并入OpenJML中。
- Junit:Java中Junit是一个单元测试包,可以通过编写单元测试类和方法,来实现对类和方法实现正确性的快速检查和测试。还可以查看测试覆盖率以及具体覆盖范围(精确到语句级别),以帮助编程者全面无死角的进行程序功能测试。
JML还有一系列其他工具,但是这些工具大都是从不同角度根据规格进行代码测试的,这些功能已被OpenJML所涵盖。
SMT Solver
SMT( Satisfiability modulo theories )求解器在形式化方法、程序语言、软件工程、以及计算机安全、计算机系统等领域得到了广泛应用。
在命令中加入-exec 可以指定使用的SMT solver的版本。
Parsing and Type-checking
OpenJML
最基本的功能就是对JML
注释的完整性进行检查。检查包括经典的类型检查、变量可见性与可写性等。通过命令行使用OpenJML
时,可以通过-check
参数(缺省)指定类型检查。
openjml [-check] options files
java -jar openjml.jar -exec Solvers-macos/z3-4.7.1 src/Person.java
使用z3-4.7.1
进行测试,没有发现错误;增加了一个符号错误后再运行发现此错误。
Extended Static Checking
为了对规格内容进行检查,需要使用-esc
参数
java -jar openjml.jar -exec Solvers-macos/z3-4.7.1 -esc src/Person.java
其中检测到了可能overflow。
Runtime Assertion Checking
使用-rac
选项可以执行运行时检查。
修改后没有发现错误。
JMLUnitNG
生成测试文件:
java -jar jmlunitng.jar src/MyGroup.java
编译文件:
javac -cp jmlunitng.jar src/*.java
测试:
java -cp jmlunitng.jar src.Group_JML_Test
可以看出,JMLUnitNG生成的单元测试,比较关注于数据边界,可以用来提醒程序员考虑数据边界。但是对于基本简单的测试,还是需要手动生成。
报错的点在没有考虑方法的传入参数为null的情况。
HW1
本次作业,需要完成的任务为实现 person
类和简单社交关系的模拟和查询,学习目标为JML规格入门级的理解和代码实现。
架构设计1
本次作业最终需要实现一个社交关系模拟系统,可抽象为一个带权无向图。
根据规格中MyPerson
的CompareTo
方法可以看出Person
由id
唯一标识,因此采用HashMap
容器管理MyPerson
和MyNetwork
,其中key
为id
。
在isCircle
方法中使用dfs
方法。
Class Diagram1
MyPerson
和MyNetwork
类主要依照JML规格所实现。
代码规模分析1
HW2
本次作业,需要完成的任务为实现 person
类和简单社交关系的模拟和查询,学习目标为JML规格入门级的理解和代码实现。
架构设计2
本次作业在HW1的基础上增加了MyGroup
。
同HW1,根据规格中MyPerson
的CompareTo
方法可以看出Person
由id
唯一标识,因此采用HashMap
容器管理MyPerson
、MyGroup
和MyNetwork
,其中key
为id
。
由于指令条数上限为100000,标程复杂度瓶颈为O(n*group_sum),n为指令条数,这次作业需要进行一些性能优化,而不是简单地按照JML的逻辑暴力实现。其中主要涉及的方法是MyGroup
中的getRelationSum
和getValueSum
方法。按照JML暴力实现的话复杂度为O(n**2)量级,如果存在多条此类查询方法将超时。因此本设计中采用了缓存操作。在MyGroup
中设置了两个属性relationSum
和valueSum
。相应的维护体现在:
在向组内加入成员时需要根据此成员的邻居更新relationSum
和valueSum
;
在添加新的关系时更新成员相应的组的relationSum
和valueSum
。
Class Diagram2
代码规模分析2
HW3
本次作业最终需要实现一个社交关系模拟系统。可以通过各类输入指令来进行数据的增删查改等交互。在第二次的基础上增加了从组中删除成员方法,财产借还方法,以及检索的相关方法。
架构设计3
同HW2,根据规格中MyPerson
的CompareTo
方法可以看出Person
由id
唯一标识,因此采用HashMap
容器管理MyPerson
、MyGroup
和MyNetwork
,其中key
为id
。
对于增加的财产借还方法,在MyPerson
中增加了money
属性。
本次作业指令条数上限为3000,标程复杂度瓶颈为O(n′m^2 +nn′logm+m^2+nm),人数为m,指令数为n,被限制的指令数为n′。因此测试重点在图论的相关算法实现。其中关于MyGroup
的缓存同HW2。
queryMinPath
方法要求实现最短路径求解。本次作业使用了堆优化的Dijkstra。其中Edge
是辅助此算法的对象。堆优化使用了PriorityQueue。
queryStrongLinked
方法要求实现点双联通分量求解。本次作业使用了平方级别的暴力算法:如果存在一个点,把它从图中删除后使得询问的两个点不连通,那么答案为否,否则为真。实现时暴力枚举所有点,把它从图中删除然后遍历即可。
Class Diagram3
代码规模分析3
总结
Test n Bugs
测试方法
本单元的测试方法主要采用Junit
。在每一个测试方法中,首先需要构造测试用例,然后使用断言判断其是否能够得到预期的结果。
测试情况
- 互测中主要采用采用
Junit
测试的办法。在第二次作业中,hack到算法复杂度高超时的bug。 - 在第三次作业中,被hack到查询双联通分量时bfs算法在加入根节点后未置根节点状态未visited,主要是因为
Junit
测试没有测试到所有类型的图。
心得体会
这一单元的工作量不大,根据JML写算法也并不困难,合理的选用容器和算法即可。
JML的撰写在刚入门时觉得无从下手,但是多看过一些,写过几个之后就很简单。这里实验课的设置很有帮助;
JML的理解在了解了基本语法之后就只靠理解力了,多想一下,动动笔总能想明白。在架构设计的时候需要建立在理解了全局的需求并且关注到具体的细节要求之上。