OO-JML第三单无博客
一、JML理论基础及应用工具链情况
JML理论基础
JML(Java Modeling Language)是用于对Java程序进行规格化设计、行为接口规格语言(Behavior Interface Specification Language,BISL)。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。
- 注释结构
//@annotation 行注释
/* @ annotation @*/ 块注释
- JML表达式
原子表达式
\result
表达式:表示一个非void
类型的方法执行所获得的结果,即方法执行后的返回值。\old( expr )
表达式:用来表示一个表达式 expr 在相应方法执行前的取值。
-\not_assigned(x,y,...)
表达式:用来表示括号中的变量是否在方法执行过程中被赋值。\not_modified(x,y,...)
表达式:与上面的\not_assigned表达式类似,该表达式限制括号中的变量在方法执行期间的取值未发生变化。\nonnullelements( container )
表达式:表示container
对象中存储的对象不会有null
\type(type)
表达式:返回类型type
对应的类型(Class)
\typeof(expr)
表达式:该表达式返回expr
对应的准确类型。
量化表达式
\forall
表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。\exists
表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。\sum
表达式:返回给定范围内的表达式的和。\product
表达式:返回给定范围内的表达式的连乘结果。\max
表达式:返回给定范围内的表达式的最大值。\min
表达式:返回给定范围内的表达式的最小值。\num_of
表达式:返回指定变量中满足相应条件的取值个数。
操作符
- 子类型关系操作符: E1<:E2 ,如果类型E1是类型E2的子类型(sub type),则该表达式的结果为真,否则为假。
- 等价关系操作符:
b_expr1<==>b_expr2
或者b_expr1<=!=>b_expr2
,其中b_expr1
和b_expr2
都是布尔表达式,这两个表达式的意思是b_expr1==b_expr2
或者b_expr1!=b_expr2
。 - 推理操作符:
b_expr1==>b_expr2
或者b_expr2<==b_expr1
。 - 空操作符
\nothing
- 全部对象操作符
\everything
- 方法规格
- 前置条件(pre-condition):
requires P
; 意为“要求调用者确保P为 真”。 - 后置条件(post-condition):
ensures P
;意为“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。 - 副作用范围限定(side-effects):使用关键词
assignable
或者modifiable
;指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。 - 纯粹访问性的方法:使用
pure
关键词 - 抛出异常子句:
signals (***Exception e) b_expr
- 类型规格
- 不变式(invariant)是要求在所有__可见状态__下都必须满足的特性,语法上定义
invariant P
,其中invariant
为关键词,P
为谓词。 - 状态变化约束
constraint
:对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。用constraint
来对前序可见状态和当前可见状态的关系进行约束。 - 方法与类型规格的关系:在大部分情况下,一个类有几种不同类别的方法:静态初始化(不是方法,但也是一种行为)、有状态静态方法、有状态构造方法、有状态非静态方法。
应用工具链情况
- OpenJML:用于检查JML文档规格语法
- JMLUnitNG:基于JML的单元测试工具,能够自动生成测试用例,注重边界条件的测试
- Junit:单元测试,可用于编写和可重复运行的自动化测试。
OpenJML验证
::openjml.bat
@echo off
set java=java -jar "%~dp0openjml.jar" -noPurityCheck
set prove=-prover cvc4 -exec "%~dp0Solvers-windows\cvc4-1.6.exe"
set runtime=-cp %~dp0jmlruntime.jar;
set allparam=
set rac=
:param
set str=%1
if "%str%"=="" (
goto end
)
if "%str%"=="-rac" (
set rac=rac
)
if "%str%"=="-prove" (
set str=%prove%
)
set allparam=%allparam% %str%
shift /0
goto param
:end
if "%allparam%"=="" (
goto eof
)
rem remove left right blank
:intercept_left
if "%allparam:~0,1%"==" " set "allparam=%allparam:~1%"&goto intercept_left
:intercept_right
if "%allparam:~-1%"==" " set "allparam=%allparam:~0,-1%"&goto intercept_right
:eof
%java% %allparam%
set filename=
if "%rac%"=="rac" (
:input
set /p filename=Input file name:
if "%filename%"=="" (
goto input
)
java %runtime% %filename%
)
pause
将上述代码放在下载有JML的文件夹中,再将该文件夹加入环境变量。理论上就可以通过命令行使用openjml
命令了。
但由于一些原因,我未能成功配置,报错图就不放了。但查阅多处博客发现OpenJML的局限,比如规格不能存在exist
,forall
。
部署JMLUnitNG/JMLUnit
未能成功配置,但查阅多处博客发现JMLUnitNG测试主要侧重于边界条件,比如对传入数据的可行域取边界进行测试。
这次测试主要依赖的还是数据生成器以及输出对拍来完成,有些规格或是测试规定的数据范围让这种测试方法更加便捷有效。而且本次作业针对一些代码实际实现可以手动构造一些特殊样例,来进行满载的测试。在类中的实现有时也不能完全按照JML规格完成,比如加入缓存之类的考量,这些使得类变为一个整体,不能单单测试某一个方法。
架构设计、模型构建策略
作业1
- 第一次作业相对简单一些,主要是阅读
Network
和Person
的JML规格,选取适合的容器来实现,因为社交关系系统的指令偏重查询,所以我在MyPerson
中的acquaintance``value
,MyNetwork
中的people
均采取hashmap
的容器来存储,利用空间换时间,提高查询效率。在Network
中查询两人是否处于同一连通块时采取bfs
的算法来完成。 - 其实在完成第一次作业时,因为对JML不熟悉,第一遍采取了Arraylist容器完全按照规格来完成代码;保证正确性后开始思考效率,最终采取map容器来提高速率。
作业2
- 这次作业主要是新增了
Group
类,因为数据测试量足够大,所以需要在保证正确性的基础上思考如何提高效率。在Group
类中采取缓存中间变量的方式,每一次往组内添加人时修改相应中间变量,将操作分散到每一次addtoGroup
中,减少了不必要的遍历。 - 需要注意或利用的是,求方差的两个数学公式,符合JML的精度规格要求,int除法的特性。
- 在这次作业中,还考虑到
HashMap
的遍历速度在大数据量下慢于ArrayList
,所以在涉及到遍历的地方均增添了ArrayList
的成员变量用来遍历操作;还对HashMap
的初始化影响速率问题做了调整,以测试量的满载按规则计算初始化值。 - 其余工作就是继承上一次作业中的代码即可,只需在NetWork中再添加对组内相关值的查询即可
- 本次作业我采取了并查集的算法,将关系圈缓存下来,加快查询速率,也为第三次作业留下了可扩展的功能。
作业3
-
这次作业难度提升巨大,主要是两个算法的考量,最短路和点双分量的求解。
-
最短路径的求解一开始采用迪杰斯特拉和弗洛伊德这两种常用的方法,但在特意构造的数据上均会被卡死,思考很久也未能想到解决办法,最后看到群里的讨论,再去搜索了相关的资料,采取了优先队列的迪杰斯特拉算法来求解路径,也不需要缓存,因为增添缓存路径会十分麻烦,而且优化后的复杂度也能够符合题目要求。
-
点双分量的求解理解起来也较为困难,仿照博客的写法构造了相关算法函数。
-
连通块数量的计算被第二次作业中的并查集很好的包含,在加人时连通块数量加1,加关系时若不联通则减1。
-
总的来说,还是在按照JML规格来实现相关内容,最终也出现像老师所说network类复杂程度较高的结果。可以将一些图相关的操作提取出来,单独成类,这样结构层次更加清晰明了。
bug和修复情况
- 这三次作业在公测互测中均未出现bug
- 在代码实现中,第二次作业的方差缓存计算时出现错误,没有考虑清楚int除法对最终结果的影响,测试发现后根据公式改变计算方式。
- 在第二次作业中对别人的代码提交了一份压力测试数据,成功hack到tle的错误。
- 在测试中还是采取生成数据以及手动构造边界数据和同学对拍的方式,这样稍微方便靠谱一些。
心得体会
- 通过JML的学习以及一单元的代码实现,我明白架构设计和代码实现是可以分开的。JML规格描述的一份架构可以让多人协作完成一个项目,而且不会出现逻辑上的漏洞。规格化设计适合用于团队开发,分工明确,可以并行推进。
- 虽然能够按照JML一行行去实现代码,但在理解意思后需要考虑真实情况下的时间复杂度和空间复杂度,寻找较好的解决方法。在第二次作业中的缓存设计尤其明显,如果一味按照JML实现,时间复杂度爆表。但有时JML也容易让人陷入思路的误区,比如第三次作业的连通块数量,在反复读JML描述后我才能反应过来是求连通块数量。
- 总的来说,以JML进行的契约式编程给我带来了新的编程体验,各个类之间在设计和调用上无需依赖具体实现,只需定义相应接口,以规格描述功能,让程序的设计与实现相分离。相信在以后的团队开发中,JML将大有作用。