- 一、JML的梳理与总结
- JML理论基础
- 应用工具链
- 二、SMT Solver的部署与验证
- 部署
- 形式验证
- 三、JMLUnitNG的部署与测试
- 部署
- 测试
- 四、梳理自己的架构设计
- 第一次作业
- 架构
- 分析模型构建策略
- 第二次作业
- 架构
- 分析模型构建策略
- 第三次作业
- 架构
- 分析模型构建策略
- 第一次作业
- 五、分析自己程序的bug
- 分析未通过的公测用例和被互测发现的bug
- bug修复情况
- 六、心得体会
- 规格撰写
- 规格理解
一、JML的梳理与总结
JML理论基础
由于很多jml博客 对理论基础总结的很好,而且指导书也会讲解基础语法,我就不再赘述。
我这里就分享一下自己在初学jml
语法时,对jml
语法总结的难点,供初学者参考:
(1) \old
- \old(
expr
)表达式:用来表示一个表达式expr
在相应方法执行前的取值。该表达式涉及到评估expr
中的对象是否发生变化,遵从Java的引用规则,即针对一个对象引用而言,只能判断引用本身是否发生变化,而不能判断引用所指向的对象实体内容是否发生变化。
(2)\sum
- 形如(\sum T x; P(x) ; k) 意思是满足P条件的x有一个表达式就加上k
(3)推理操作符
-
只要不是结果false条件true就成立
-
b_expr1==>b_expr2
或者b_expr2<==b_expr1
。对于表达式b_expr1==>b_expr2
而言,当b_expr1==false
,或者b_expr1==true
且b_expr2==true
时,整个表达式的值为true
。
(4)断言
- 特别需要提醒,在
JML
断言中,不可以使用带有赋值语义的操作符,如++,--,+=
等操作符,因为这样的操作符会对被限制的相关变量的状态进行修改,产生副作用。
(5)多个requires子句是并列,都得满足
- 注意,方法规格中可以有多个requires子句,是并列关系,即调用者必须同时满足所有的并列子句要求。如果设计者想要表达或的逻辑,则应该使用一个requires子句,在其中的谓词P中使用逻辑或操作符来表示相应的约束场景:
requires P1||P2;
。
(6)不变式invariant
- 在方法执行期间,对象的不变式有可能不满足(是不可见状态)。因此,类型规格强调在任意可见状态下都要满足不变式。
应用工具链
可以应用的主要jml工具是OpenJML
、JMLUnitNG
:
-
OpenJML:首选的JML相关工具,以提供全面且支持最新Java标准的JML相关支持为目标,能够进行静态规格检查(ESC,Extended Static Cheking)、运行时规格检查(RAC,Runtime Assertion Checking)和形式化验证等一系列功能。OpenJML提供了自带的命令行版本和Eclipse插件版本。
-
JMLUnitNG:JMLUnit的替代工具,能够根据JML规格自动生成基于测试库TestNG的单元测试集。
我看了前人的博客 ,得知IDEA对JML
插件的支持并不好,就下载了一个Eclipse
专门来配置插件。
配置的方法参考了这篇配置Eclipse插件的博客 ,在此表示感谢。我看的博客的电脑是windows
操作系统,我的电脑是deepin
操作系统,本以为因为操作系统不同会出一些问题。不过,下载插件只要在Eclipse里面输入下载地址就可以下载好,甚至不需要选择不同操作系统的文件下载类型,一键安装就完成了,非常方便(如下图)。
最后下载好发现有很多功能(如下图),我目前只会用其中的静态规格检查(ESC)和形式化验证功能,更多的功能有待掌握。
二、SMT Solver的部署与验证
部署
在Eclipse
安装好OpenJML
插件之后,就其实已经帮我们下载好SMT Solver
了。
我在Eclipse
的plugins
文件夹里面发现了一个叫org.jmlspecs.Solvers_1.6.0
的文件夹(如下图),打开一看里面就有需要用的Solver
——z3
。
在Eclipse里面Window -> Preferences -> OpenJML -> OpenJML Solvers->设置配置,把路径定义为z3
的路径。
形式验证
在Eclipse的JML
菜单里选择静态规格检查(ESC)功能,可以对目标的java文件进行形式验证。
我对第一次作业的MyNetwork.java
进行了形式验证,发现addPerson这个方法是不符合形式验证的。
黄色高亮信息显示的是:
Method addPerson overrides parent class methods and so its specification should begin with 'also'
[INVALID]信息显示的是:
注意到信息里面的assertion
出错的是PossiblyNullDeReference
,应该是有null
指针的问题。
三、JMLUnitNG的部署与测试
部署
本人的部署操作是借鉴这篇博客 的,在此表示万分感谢,通过这个方法无报错地实现了JMLUnitNG测试。
1. 在官网上OpenJML的压缩包,下载JMLUnitNG的jar包,放到OpenJML的文件夹里。
2. 运行`java -jar openjml.jar "$@"`可以看到能用的对应jar包功能
3. 新建一个文件夹test,里面放入需要自动生成测试的java文件
4. 运行`java -jar jmlunitng.jar demo/*.java`会在test文件夹生成一系列测试文件
5. 编译所有文件执行命令`java -jar openjml.jar -rac `,得到rac文件
6. 执行命令`java -cp jmlunitng.jar test/Demo_JML_Test`,就可以看到测试结果
测试
测试结果如下:
用的测试类是Group接口里2个比较简单的方法构成的,所以自动生成的8个测试点都通过了。
发现测试用例生成的都是边界上的数据,比如针对整型变量的0、2147483647、-2147483647。
这种边界数据的测试可以帮助我们发现自己的错误,比如溢出、除0之类的错误。
四、梳理自己的架构设计
第一次作业
架构
分析模型构建策略
由于第一次作业比较简单,所以我只是在容器的选择上有自己的设计。
存储人的容器不是用jml
规格写的数组,而是用hashmap
来存储从人的id到认识的人的映射。这样查找起来可以利用hash值查找的优势,避免遍历数组查找。
第二次作业
架构
分析模型构建策略
第二次作业增加了很多指令,而且规定了时间约束,所以要开始考虑算法的复杂度问题了。
我采用了缓存的策略。
对于group类的查询方法,可以大致分为2类:
- 直接返回一个
sum
值,比如getValueSum
- 不是直接返回一个
sum
值,比如getAgeVar
但是这2者都是要用到缓存的信息的,所以缓存的时候,我缓存了
private int oldPersonListLen;
private int relationSum;
private int valueSum;
private BigInteger conflictSum;
private int ageSum;
这5个信息,是足够满足题目要求的。
所以,每当来一个group类的查询请求,我先检查是不是更新(比对oldPersonListLen
与现在的personList.size()
是否相等)
- 如果更新了,就把缓存的5个值更新一波。再返回查询值。
- 如果没更新,直接返回查询值。
返回查询值的时候
- 如果是类似
getValueSum
的,就可以直接返回。 - 如果是不能直接返回的,类似
getAgeVar
,就当场根据公式算一遍返回值。
这样的策略只是为了省一些计算时间,防止ctle
。
第三次作业
架构
分析模型构建策略
第三次作业的难度,比前2次增加了很多,有一些复杂的图论算法需要我们去掌握,我尽量采用的是简单而不易出错的策略。
(1) isCircle
前2次作业的isCircle
都采用的是bfs
去找是否联通,但是这次作业由于有其他方法要调用多次isCircle
,就不得不考虑更快的方法了。
我使用了群友推荐的并查集的方法,单独写了一个并查集的类,衔接到原来的架构里。
并查集搜联通果然很好,速度又快,又好写。
(2) queryBlockSum
这个查询联通分量的方法也可以用并查集做,只要遍历所有的人,看有多少种不同的树根,速度也很快。
(3) queryStrongLinked
我把原来的 isCircle
的bfs
版本留下了,改成返回一条联通的边。
这样就可以用到queryStrongLinked
里面
所以就先考虑id1
与id2
直接相连的情况。再判断id1
与id2
是否连通,再枚举去掉这条边上其他n-2个点,每次去掉一个点,判断id1
与id2
是否连通。如果枚举完了,都是连通,说明id1
与id2
满足。
(4) queryMinPath
单独写了一个Dijkstra
类来实现优先队列的Dijkstra
算法,并且用了一个新的Edge
类来记录边的信息,计算最短路径。
群里还有人说可以不把起点到所有点的最短路径算完,只要算到目的地就可以了。但是我还是没有改,算完所有点,最后时间还是够的。
五、分析自己程序的bug
分析未通过的公测用例和被互测发现的bug
(1)强测
三次强测都没有发现bug。
(2)互测
在第二次互测中被hack了9次,发现是其实是一个"莫须有"的bug。
因为从第一次作业过渡到第二次作业的时候,我把每个方法的jml规格
都重新看了一遍,就害怕有改变。
果然发现了第一次作业中Network的addRelation
方法有如下的JML
:
@ also
@ public normal_behavior
@ requires id1 == id2 && contains(id1);
@ assignable \nothing;
但是在第二次作业里这个被删掉了。然后,讨论区里的助教就说"如果规格中没有满足目前条件的分支,则此方法不能执行任何操作,应直接return"
我把原来的 id1 == id2 && !contains(id1)
就报错的语句给删掉了,改成直接return。
果然,强测遵守jml
约定,没有出现 id1 == id2
。
但是,互测时,这个约定就貌似被忽略了。
于是,大伙都可以提交addrelation id1 == id2 && !contains(id1)
的指令来hack我QAQ
。
bug修复情况
第二次互测,由于规格约束的冲突,我就只能又改回去第一次作业的规格要求的版本提交bug修复了。
六、心得体会
规格撰写
- 撰写规格的时候,由于只能用数组表示数据集合,没有容器,所以要从跟抽象的角度思考描述。
- 可以通过调用已有的方法来撰写规格,避免重复描述。
规格理解
- 在理解规格的时候,切忌照抄规格,要根据规格用合适的数据类型和实现方式,写出自己的代码。
- 可以通过
JUnit
测试来检查自己对规格的理解是否准确。 - 也可以进行
形式验证
来检查自己对规格的理解是否准确。