本单元就JML的阅读使用及其相关工具链的使用做了相关学习和训练,下面从几个方面针对本单元内容进行一个小小的总结,也算是给这单元一个月来的学习划上一个句号。
1 JML理论基础及其工具链梳理
1.1 JML理论基础
JML(Java Modeling Language)是一种行为接口规格语言(Behavior Interface Specification Language,BISL),基于Larch方法构建。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。
上面是关于JML的一个简单定义。本单元就我个人的体会而言,使用JML有两点最大的好处。一是使用逻辑严格的规格对将代码的要求对实现人员进行说明,以保证最终实现效果和所期望的一致,即开展规格化设计,本单元的三次作业我们都是在助教的JML规格基础上进行功能实现的。二是针对已有代码的实现,在已有代码的基础上书写其对应的JML的规格,从而可以提高代码的可维护性,我们也在第一次实验中对这方面进行了训练。
1.1.2 语法介绍
-
JML以
javadoc
注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。其中行注释的表示方式为//@annotation
,块注释的方式为/* @ annotation @*/
。 -
JML表达式是对Java表达式的扩展。具体可分为原子表达式,如
\result
,\old()
等,量化表达式,如\forall
,\exists
等、集合表达式。这些表达式和操作符一同完成了对JML具体内容的表达。 -
方法规格是JML的重要内容。方法规格的核心内容包括三个方面,前置条件、后置条件和副作用约定。
-
前置条件(pre-condition):通过requires子句来表示:
requires P;
。方法的调用者应当保证谓词P为真,否则对于该输入,方法不按照对应的规格进行输出。 -
后置条件(post-condition):通过ensures子句来表示:
ensures P;
。当输入满足前置条件时,方法应当保证谓词P为真。 -
副作用范围限定(side-effects):。JML提供了副作用约束子句,使用关键词
assignable
或者modifiable
。副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。从方法规格的角度,必须要明确给出副作用范围。 -
signals子句:signals子句的结构为
signals (***Exception e) b_expr;
,意思是当b_expr
为true
时,方法会抛出括号中给出的相应异常e。
-
-
类型规格:针对Java程序中定义的数据类型所设计的限制规则,一般而言,就是指针对类或接口所设计的约束规则。
-
不变式(invariant):要求在所有可见状态下都必须满足的特性,语法上定义
invariant P
,其中invariant
为关键词,P
为谓词。 -
状态变化约束:对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。
constraint P
之中的P对当前状态和前序状态之间的关系进行约束。
-
1.2 工具链介绍
-
OpenJML:OpenJML是一个相对完整的JML工具,提供了JML语法检查、基于SMT Solver的静态检查(ESC)和基于自动生成测试用例的运行时检查(RAC),可用于检验JML规格的正确性。
-
SMT Solver:用于验证代码和规格的等价性。
-
JMLUnitNG/JMLUnit:可以基于JML规格自动生成测试用例。
2 SMT Solver
在对JML规格进行了补充和修改后,使用OpenJML
工具对Person
类中的几个方法进行了测试。
测试时,我使用了命令
java -jar .\openjml.jar -exec .\Solvers-windows\z3-4.7.1.exe -esc .\unit3task1\src\homework\Person.java
对Person
类中的getId()
、getName()
、getAge()
、equals()
等方法进行了静态检查。其中:
-
exec参数用于指定Solver可执行程序
-
esc参数指定检查类型为Extended Static Checking。
运行结果如下:
由于openjml
比较古老,我们现在使用的很多JML
语法无法被识别,因此如果要真正利用OpenJML
来帮助我们测试的话还需要等待OpenJML
的进一步改进。
3 JMLUnitNG/JMLUnit
利用JMLUnitNG
对Group
中的方法进行了自动生成数据的测试。
测试时使用如下的命令进行测试:
java -jar jmlunitng-1_4.jar ./test/Group.java javac -cp jmlunitng-1_4.jar ./test/*.java openjml -rac ./test/Group.java java -cp jmlunitng-1_4.jar Group_JML_Test
得到了如下输出结果:
Passed: constructor Group() Passed: <>.addPerson(java.lang.Object@490d6c15) Passed: < >.addRelation() Passed: < >.delPerson(java.lang.Object@7bfcd12c) Passed: < >.equals(java.lang.Object@6956de9) Passed: < >.getAgeMean() Passed: < >.getAgeVar() Passed: < >.getConflictSum() Passed: < >.getValueSum() Passed: < >.hasPerson(java.lang.Object@2d6a9952) Passed: < >.size() Passed: < >.getId() Passed: < >.getRelationSum()
可以看到测试点许多是针对极端边界数据情况下的测试。
4 作业架构设计分析
4.1 第一次作业
第一次作业我的架构实现的比较简单,直接按照所给的三个接口实现了三个类。本次作业在存储acquaintance
和people
时我均采用了较为简单的arraylist
,在实现MyNetWork
中的isCircle
方法时采用了dfs
进行深度优先搜索。
4.2 第二次作业
这次作业相对于上次作业新增了Group,在作业架构上我新增了基于Group
接口的MyGroup
类。在具体实现方面:
-
在存储容器方面,为了方便遍历和查询,我对于需要存储的对象都同时采用了
Arraylist
和Hashmap
的存储方式,通过牺牲空间的方式以期望达到操作时节省时间。 -
为了提高
MyGroup
中的getRelationSum()
、getValueSum()
、getConflictSum()
、getAgeMean()
等方法的查询速率,在addPerson()
方法时提前对需要用到的ageSum
、conflictSum
、relationSum
等数据进行缓存,以提高这些方法的时间效率。 -
由于使用
dfs
实现isCircle()
方法时时间不太理想,故在本次作业中改为由bfs
来实现。
4.3 第三次作业
本次作业的大体结构上还是采用的第二次作业的框架,增加了一个Edge
类以方便实现迪杰特斯拉算法。本次作业MyNetWork
类中不少涉及图论的算法都有难度,在具体实现方面:
-
isCircle()
方法改为了并查集实现,相比于前两次作业所用的方法省去了许多遍历的操作,并且实现起来也更为简单,由于并查集还实现了路径压缩,因此大大缩短了所需要的时间。 -
queryMinPath
()方法:由于本次作业性能要求较高,因此该方法采用了迪杰特斯拉算法+堆优化的实现,以达到最好的时间效率。 -
queryStrongLinked()
:为了正确性的考量,在实现该方法时本人采取了暴力的删点后再判断是否连通的方法,时间效率在课程组所给的数据范围内也能通过。 -
queryBlockSum()
,由于本次作业实现了并查集,在该方法时只用查找有几种根节点并返回其数目就可以了,操作简单且时间效率高。
5 BUG分析
5.1 公测
-
第一次作业中由于
dfs
实现时对visited
数组实现的略有问题,导致会多遍历很多节点,导致强测中出现了超时。修复时简单地修改了对visited
的判断和标记BUG就得以了解决。 -
第二次作业中由于未对
getAgeMean()
方法进行缓存,该方法每次都会进行遍历。而在getAgeVar
时又错误地重复多次调用了getAgeMean()
导致出现了超时。修复时针对所出现的问题进行修改后BUG得以解决。 -
第三次作业强测中针对
qmp
测试的点出现了超时,略微超过了2s,但我将数据下载到本地测试所用时间并不会超过1.6s,并且BUG修复阶段未对queryMinPath()
方法进行修改,提交后原来的超时错误点也修复了。很疑惑为什么强测会测出超时。
5.2 互测
-
第一次作业中Hack到别人判断
isCircle()
错误的BUG,自己被Hack到了和强测同样的因为dfs
没有正确实现而导致的超时。 -
第二次作业中Hack到了别人因为没有缓存数而导致的超时错误,自己也被Hack到了和强测相同原因的超时错误。
-
第三次作业中Hack到了别人的超时错误以及数组越界错误,自己被Hack到了
delPerson()
时未删除person对应的缓存而导致的错误
6 心得体会
本单元引入了JML这一有力的工具,通过利用JML逻辑严格的规格可以对代码的需求进行比较好的描述。
首先是在第一次实验中对于规格的撰写我个人认为还是很有难度的,给我们一个已知的代码,让我们对他进行规格描述,怎样做到利用JML这一工具进行准确、无歧义的描述是一个值得研究的问题,总的来说JML是一个十分有用的工具,将代码编程可形式化验证的东西,极大地方便了代码的需求者和实现者。
就这单元的三次作业而言,每次作业或多或少都有些遗憾。虽然每次作业完成后也在与同学积极对拍、优化算法,信心满满地提交上去,但总是会出现想不到的地方发生超时。这说明算法的基本功以及对java的理解等方面还存在着一定的问题。尤其是第三次作业,涉及到的迪杰特斯拉算法、最优堆、并查集等都还非常地不熟悉,都需要查找相关资料重新学习一遍才能够运用,不由得感慨当时学数据结构时没有非常认真地掌握每一个知识。
这持续将近一个月的第三单元也已划上了句号了,虽然有不少的遗憾,但通过这一单元学习了不少知识也算是很大的收获了。希望第四单元时能够花更多的时间思考代码实现,不要再像这次作业一样急于实现代码而忘记了思考代码架构的合理性。
最后感谢各位助教及老师在这一单元的付出!