一、JML语言理论基础与应用工具链梳理
理论基础
JML是用于对Java程序进行规格化设计的一种表示语言。通过类似离散数学里面的语句,规范明确地指出操作的行为。
具体而言,JML有两种主要的用法。
(1)开展规格化设计。这样交给代码实现人员的将不是可能带有模糊性的自然语言描述,而是逻辑严格的规格。
(2)针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在代码的维护方面具有特别重要的意义。
显然,这样可以更好地便利开发人员之间的交流,提高程序开发的效率与程序维护的可行性。通俗地讲,JML就是一种在Java代码中规范化添加的注释,在使用JML时,我们可以不考虑具体实现的途径而描述方法的预期功能。只要能够满足JML的要求,就能够保证程序的正确性。
1.注释结构
在写JML时,如果是方法的规格,要写在方法的上方;如果是invariant
或是实例成员等,则在类一开始就要写出。JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方法,分别是行注释和块注释。行注释要写成//@ annotation
的形式,块注释写成/* annotation */
的形式。
2.常用的表达式
- 原子表达式
- 量化表达式
- 操作符
等价关系操作符:<==>, <=!=>
推理操作符:== >,< ==
变量引用操作符:\nothing, \everything
3. 方法规格
-
前置条件:以
requires P
的形式出现,其中requires是JML关键词,表达的意思是要求调用者保证P为真。一个方法规格可以有多个requires
语句,彼此之间为并列关系。 -
后置条件:以
ensures P
的形式出现,其中ensures是JML关键词,表达的意思是要求方法执行完毕后P为真。一个方法规格可以有多个ensures语句,彼此之间为并列关系。
-
副作用:副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据。以
assignable
或modifiable
的形式出现。
-
异常:以
signals
的形式出现,对方法需要抛出的异常给出明确指出。
4. 类型规格
- 不变式:
invariant
要求所有在可见状态下都满足的条件。要求在所有可见状态下都必须满足的特性,语法上定义 invariant P ,其中invariant 为关键词, P 为谓词。 - 状态约束变量:
constraint
要求状态变化过程中满足的条件。对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。JML为了简化使用规则,规定invariant只针对可见状态(即当下可见状态)的取值进行约束,而是用constraint来对前序可见状态和当前可见状态的关系进行约束。
5、应用工具链
- Openjml:OpenJML是Java程序的程序验证工具,使用SMT Solver来对检查程序实现是否满足所设计的规格。下载地址:http://www.openjml.org/
- JUnit:JUnit是一个Java语言的单元测试框架。可以在其中编写自己的单元测试代码,针对性地测试代码中的某一部分。
- JMLUnitNg:可以针对JML自动化生成测试用例,用例比较偏向于极端化。官网地址:http://insttech.secretninjaformalmethods.org/software/jmlunitng/
二、部署SMT Solver
Openjml的使用我参考的是https://www.cnblogs.com/lutingwang/p/openjml_basic.html这篇文章,但是openjml这个软件的问题有些多。。。找了不少资料才弄成功(捂脸哭),全是报错简直自闭。
另外,本次使用的SMT Solver为z3。
z3的安装和使用可以参考这两篇文章。
1、https://blog.csdn.net/weixin_41529962/article/details/80274125
2、https://blog.csdn.net/weixin_41529962/article/details/80295088
public class Test {
public static void main(String[] args) {
System.out.println(Test.add(520, 521));
System.out.println(Test.div(10,5));
System.out.println(Test.mod(10,3));
}
//@ensures \result == a + b;
public static int add(int a, int b) {
return a + b;
}
//@ensures \result == a / b;
public static int div(int a, int b) {
return a / b;
}
//@ensures \result == a % b;
public static int mod(int a, int b) {
return a % b;
}
}
这样通过测试可得到对应的反馈信息,为两条INVALID报错,提示我们div和mod无法处理b为0的情况,显然这种报错是正确的。
三、部署JMLUnitNG/JMLUnit
这个软件的使用比较方便,把压缩包和代码放在openjml的目录下就可以了~
测试结果如下:
分析自动生成的测试样例可以发现,JMLUnitNG生成的测试样例一般只能测试极端情况,假如参数是int就测试0,-2147483648和2147483648这三种情况,如果参数是object就测试null。显然这样的测试是无法保证完全度的,只能测试一下极端情况是否满足,仅此而已,实用价值不大。真正全面的测试还得是使用Junit手写测试样例。
四、代码架构设计
第九次作业
这次作业是我第一次接触JML,最大的难点在于如何读懂JML的内容。在听了视频课之后又对照着手册尝试性地去实现了一下发现貌似是比较直观的。主要的问题是要解决使用哪种方式实现更为合适。难点函数isCilrcle在这一次当中使用的是dfs方法,这在本次作业还是可行的,但是人和关系多了就存在可能的超时问题。其他的要思考如何实现的地方就是数组究竟要使用哪一种方式实现,我在这一次作业使用的都是arraylist,这为后面留下了一些隐患。
第十次作业
这次作业是在上一次的基础上增加一部分函数。我认为最大的问题就是时间复杂度,居具体的功能实现起来是比较简单的,但是怎么样能让时间尽可能的短是很考验人的。group里面getXXXsum类的函数不能单纯的调用一次算一次,必须使用缓存的方式,提前维护一个相关的成员变量,在需要的时候直接使用这个值,比如getConflictSum,需要定义一个conflictsum的BigInteger成员变量,在每次加入人的时候更新,在调用函数的时候直接返回,这样大大降低了时间复杂度。
另外一个要大改的是iscircle这个函数,如果单纯使用dfs很容易就tle了,我最终决定使用并查集的方式,并在查询途中优化树的结构,使其左右枝基本均衡,进一步降低时间复杂度。
第十一次作业
这次作业依旧是增加新的函数。主要是求最短路径,求是否双连通分量,求连通块个数这三个函数最复杂。
求最短路径的queryMinPath函数我的具体实现方式是堆优化的迪杰斯特拉算法,这里要注意的是单纯使用迪杰斯特拉函数可能在数据量过大的时候出现超时,在java中恰好有priorityQueue这个优先队列类,所以直接使用就很方便。
求两个点是否在同一个点双连通分量的queryStrongLinked函数,我采用的方法是tarjan算法,这个算法网上资料很多,但是比较容易出错。这里要注意的是如果使用两次dfs遍历,在第一次找到点之后删除经过的点再dfs一次这种方法,极大可能出现tle甚至是WA的问题!!!我自己就踩了这么个大雷!!
求连通块数目的queryBlockSum函数,在实现时借用了isCircle中使用的并查集,显然该函数的返回值就是并查集中不同根节点的个数,也是比较简单的。但是!!一定一定不能单纯按照JML规格的描述直接去写,双重循环百分百爆炸。。。。强测炸了好几个点的我如是说。。。。。。
bug查找与修复
在前两次作业中强测互测都没有问题,第三次作业失误强测炸了不少点,都是tle的。
具体的查找bug方式主要还是和同学对拍,因为这个单元的作业输出比较直观且唯一,所以跑大量随机数据对拍的方式最为简单有效。
三次作业最主要也最致命的问题就是超时,正确性的问题很容易查出,修改时通常再读一下JML也就改好了,但是tle真是隐形杀手!!解决办法是询问同学哪种算法时间复杂度更低,然后找资料重写。
互测中查找别人bug最主要的方式就是针对tle的压力测试,这在第二次和第三次作业里面让我收获颇丰。还有一个hack到别人的bug是第三次作业中,对于部分使用了tarjan的同学,可能忽视两个点直接相连但不是点双连通的情况,这是我在课下查到的自己的问题,互测中果然也发现了其他同学有这样的问题,改正方法很简单,就是判断一下两个点所在的点双连通分量拥有的点的数目是否大于2即可。
心得体会
第三单元的作业在开始之前我是满怀信心的,因为这个单元没有性能分了!在经历了前两个单元为了性能分不停地优化,查算法,卷来卷去只为了那性能分的痛苦经历之后,总算是只要AC就好了。但是在实现的过程中却发现并不是我想的那样,算法的时间复杂度依旧可能使得程序拿不到分,我在前两次作业中做的还是不错的,能兼顾正确性和时间复杂度,但是第三次作业可能是有些飘了。。。。忽视了一个很重要的函数的实现方式,直接按照JML就写了,对拍的时候也没注意到这一点,一出强测分真是心态崩了。
这个单元的学习给我一下几个感悟:
1、JML是个好东西,以一种逻辑条理的方式规定了方法实现者要做的事情,大大降低了程序不同阶段开发者之间沟通交流的难度。
2、对于JML不能完全照搬,JML说白了就是在无数可能的实现方式里面选一个来告诉你你要干什么,并不是说就一定按照JML说的那个方式实现。一方面程序在编写是还是要照顾复杂度的,JML的方法并不一定是最佳的实现方式,另一方面,程序本身是个整体,各部分之间是有联系的,如果完全照搬JML,很有可能写完的程序各个方法完全独立了,没有彼此之间的交流,这样不利于程序的协调,也势必会让程序整体变得复杂。
3、写程序,不能飘!!前两次完成的不错,导致我第三次在实现是少了些谨慎的思考,单单是把程序实现了,最后因为tle挂了不少点,非常遗憾。。。好气啊