JML 规格化设计总结
我们进入了规格化设计单元——JML(Java Modeling Language)。本单元整体感受要比之前两个单元的工程量小了很多,但是不要小看了JML的威力(还是很容易出错的....对离散数学以及数据结构的基础知识要进行回顾)。
本单元要实现从程序的设计者角度,向程序实现者角度的转变。在理解架构的基础上,满足契约,进行自己的模块算法的设计。规格化设计在工程实现领域还是有很重要的地位的。掌握规格化设计能够为我们以后在工作岗位团队协作,提高代码质量带来很多便利。下面我将对本单元作业进行梳理。
- JML 规格化设计总结
- JML理论知识与工具链
- JML理论知识
- JML工具链
- OpenJML与JMLUnitNG实现自动生成测试用例
- openJML部署与规格静态检验
- JMLUnitNG自动生成测试
- 架构设计及bug分析
- 心得体会
- JML理论知识与工具链
JML理论知识与工具链
JML理论知识
下面我对这三次作业、两次实验多次出现最精简的JML理论知识进行总结。详细的内容可以参考课程组下发的课程学习ppt和JML Level0手册。
1.注释结构
行注释的表示方式为 //@annotation
,块注释的方式为/* @ annotation @*/
。
2.JML表达式
原子表达式
\result
:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。\old( expr )
:用来表示一个表达式 expr 在相应方法执行前的取值。\not_assigned(x,y,...)
:用来表示括号中的变量是否在方法执行过程中被赋值。
量化表达式
\forall
:(对应数学中的任意符号)全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。\exists
:(对应数学中的存在符号)存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。\sum
:返回给定范围内的表达式的和。\max
:返回给定范围内的表达式的最大值。
操作符:
- 推理操作符:==>
3.方法规格
- 前置条件(pre-condition):requires,进入类或方法的参数应该满足的要求
- 后置条件(post-condition):ensures,经过类或方法处理后的变量有何变化
- 副作用范围限定(side-effects):assignable,此类或方法引起的变化
- signals子句:signals,对应于exception_behavior,抛出异常
- 当然设计中也常用pure关键词,表示纯粹访问的功能方法
4.类型规格
- 不变式invariant:在所有可见状态下都必须满足的特性(白话就是不用再在每个方法的规格里面都对变量
ensures
了。避免了在方法中对对象造成改变。) - 状态变化约束constraint:对变量的变化进行约束。(侧重于变化)
JML工具链
“工欲善其事,必先利其器”
-
OpenJML
OpenJML
最基本的功能就是对JML
注释的完整性进行检查。检查包括经典的类型检查、变量可见性与可写性等。通过命令行使用OpenJML
时,可以通过-check
参数(缺省)指定类型检查。点此下载
-
JMLUnitNG
配合上面说到的
OpenJML
可以根据JML自动生成TestNG
测试文件的工具点此下载
-
SMT Solver
用来证明代码逻辑等价的,也就是可以用来从形式上证明两个函数的效果是等价的
OpenJML与JMLUnitNG实现自动生成测试用例
openJML部署与规格静态检验
目录结构如下:
cmd命令如下
java -jar openjml的路径 -exec 解释器的位置 -esc 需要验证规格的文件的路径
对Person.java进行分析通过测试
下面我们再举一个比较常见的例子:
public class testJML{
public static void main(String[] args) {
testJML testjml = new testJML();
}
//@ensures \result == a * b;
public int mul(int a, int b) {
return a * b;
}
//@ensures \result == a / b;
public int div(int a, int b) {
return a / b;
}
//@ensures \result == a % b;
public int mod(int a, int b) {
return a % b;
}
}
检验结果出现除0,模0以及乘法大小限制的警告。
JMLUnitNG自动生成测试
此处为jyc大佬博客的链接
依次输入以下指令:
java -jar jmlunitng-1_4.jar test\MyGroup.java
值得提出的是,在IDEA中我们经常缺省,但是在检验的时候会出现error。所以要把泛型补全。
javac -cp jmlunitng-1_4.jar test\*.java
生成一堆.class文件
java -cp jmlunitng-1_4.jar test.MyGroup_JML_Test
自动测试效果还是非常好的,自动生成的数据大部分是一些边缘数据。我们可以看一下Failed的点,是addPerson和delPerson,原因是在规格中没有给这两个方法设定限制条件。限制条件在NetWork.java中,本次没有测试,所以会出现null的错误。
架构设计及bug分析
第一次作业
第一次作业我按照代码仓库给出的代码架构框架进行设计。如下图所示:
bug分析
第一次作业整体上比较简单。但是,我却出现了大量的CTLE。问题出现在isCircle
方法,即判断两点是否连通的处理上。我使用了DFS
算法,以下为错误示例:
// 关注对path的操作
path.put(indexPerson.getId(), indexPerson); // 进入函数入栈
if (indexPerson.equals(aimPerson)) {
HashMap element = new HashMap<>(path);
paths.add(element);
path.remove(indexPerson.getId()); // 出栈
return true;
} else {
HashMap acquaintance = ((MyPerson) indexPerson).getAcquaintance();
Iterator> iterator = acquaintance.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();
if ((path.get(entry.getKey()) == null)) {
flag |= Dfs(entry.getValue(), aimPerson);
}
}
path.remove(indexPerson.getId()); // 出栈
return flag;
}
}
上方DFS
代码看似正确(我最初也一直是这样认为的),但是却存在致命的问题——$O(n!)$ 复杂度。举一个简单的例子,如下图:
我想判断1到5是否连通,那如果我按照上方代码进行寻找的话,我找到1234,1243,1324,1342,1423,1432,15。很明显可以看出进行了许多无效查找。仅仅是一个简单的图就已经做了许多无效查找,如果图的复杂度更高,超时也不是冤枉。这说明仅仅维护栈是不够的。
其实优化也很简单,就是做一个visit
数组,保存已经访问过的顶点即可。这样遍历就变成了1234,15。变回了我们的老朋友——“善良的”DFS
。
关于找到别人的Bug,由于我进入的是C屋,基本上随机生成数据就有很好的hack效果.....同屋子的同学基本都是超时问题,我研究他们的代码,很多isCircle和我设计的思想完全一样。(说明这个问题其实也属于很多基础不扎实的同学的共性)
第二次作业
第二次作业加入了Group
,即关系群。
我是按照给出的框架填充的。这次作业比较简单,重点是我理解到不可以仅仅是按照JML规格给的格式设计。如下图:
如此简洁美丽的注释,按照注释写不就行了?
那恭喜你!超时了!
本次作业只要用到缓存数据的思想即可——在数值改变的时候,对数值进行更新;读取的时候就可以达到$O(1)$的复杂度。
因为迭代开发,代码量比较小。并未发现自己与别人的bug。
第三次作业
第三次作业果不其然要考到图算法。本次涉及到了:最短路径问题、双向联通问题以及连通块个数问题。
最短路径问题我应用了dijkstra算法。为此我多设置了一个Item类,即PriorityQueue存放的对象。Java已经自带PriorityQueue方法,用起来很方便,只需要自定义以下比较规则就可以了。
双向联通最好的解决方法是tarjan(塔扬)算法。但是由于听闻比较容易出错,我应用了暴力求解算法...然后超时了。优化后采用了两次DFS,每次记录下连通的路径个数。对路径个数进行判断。选取第一次遍历的通路中最短的,对每个点分别进行Mask,再次DFS,如果仍然可以连通,那就可以了。一定注意分别对每个点mask!
连通块的个数可以在ap是+1,ar时如果!isCircle
,就-1。同时把isCircle
改为并查集。(还可以用缓存的思想进一步优化,回想以下计组的dirty
)
hzx大佬关于算法复杂度的分析
本次还是出现了超时错误。确实没想到强测会是举基本所有3000条指令测特定的指令orz。互测的时候我们屋非常安静....活跃分扬了...
心得体会
丰富的心理变化:
OO为什么突然这么简单了???简直不可思议!!!中测一遍过???
原来不是我变强了,是中测变弱了,满屏的超时....甚是害怕
提心吊胆搞优化,明白了不能仅仅按照JML写的去实现。JML只是为我们提供了一个输入输出的契约,但是算法与数据结构还是要自己去选择。
OO课程也过去了一大半了,觉得这门课程虽然比较难,但是确实还是很有收获的。
在不断优化的过程中,对容器的了解也进一步加深。HashMap这种比较快的容器,可以在恰当的时候多使用。每种容器都有自己的适用条件与优缺点,不是ArrayList解决一切。
JML规格可以规范自己的设计,要认真读懂,关注细节,但具体实现还是要靠自己的理解。不要逐字逐句去翻译。
最后,行百里者半九十,认真对待最后一个单元!