一、JML的理论基础和应用工具链
JML的理论基础
JML(Java Modeling Language)是一种形式化的、面向JAVA的行为接口规格语言。
本单元课程作业是由课程组提供JML,而且确保只要代码严格满足JML的要求,则正确性就可以保证。这就是一种契约式编程的过程,设计者提供规格设计,不关心代码的具体实现;实现者只要满足规格的要求,可以自由的选择代码的实现方式,不需要被固定思路束缚。这种契约式编程可以在保证各个环节正确性的前提下,使设计者不必关注具体实现,实现者也不需要关注整体结构,降低了工程的耦合度,减轻了各个环节的负担。
JML表达式
原子表达式:
关键字 | 含义 |
---|---|
\result | 表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。 |
\old(expr) | 表示一个表达式expr 在相应方法执行前的取值,该表达式涉及到评估expr 中的对象是否发生变化。 |
\not_assigned(x,y,...) | 用来表示括号中的变量是否在方法执行过程中被赋值。如果没有被赋值,返回为true ,否则返回 false 。用于后置条件的约束。 |
\not_modified(x,y,...) | 该表达式限制括号中的变量在方法执行期间的取值未发生变化。 |
\nonnullelements(container) | 表示container对象中存储的对象不会有null。 |
\type(type) | 返回类型type对应的类型(Class),如type(boolean)为Boolean.TYPE。 |
\typeof(expr) | 该表达式返回expr对应的准确类型。如\typeof(false) 为Boolean.TYPE。 |
量化表达式:
关键字 | 含义 |
---|---|
\forall | 全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。 |
\exists | 存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。 |
\sum | 返回给定范围内的表达式的和。 |
\product | 返回给定范围内的表达式的连乘结果。 |
\max | 返回给定范围内的表达式的最大值。 |
\min | 返回给定范围内的表达式的最小值。 |
\num_of | 返回指定变量中满足相应条件的取值个数。 |
操作符:
关键字 | 含义 |
---|---|
Java定义的所有操作符 | 同Java |
\nothing | 否定表示当前作用域访问的所有变量。 |
\everything | 表示当前作用域访问的所有变量。 |
JML方法规格
- 前置条件
对方法输入参数的限制,如果不满足前置条件,方法执行结果不可预测,或者说不保证方法执行结果的正确性。
例 :
requires P;其中requires是JML关键词,表达的意思是“要求调用者确保P为真”。多个分开的requires是并列关系都要满足,或关系用requires P1||P2;
- 后置条件
对方法执行结果的限制,如果执行结果满足后置条件,则表示方法执行正确,否则执行错误
例 :
ensures P;其中ensures是JML关键词,表达的意思是“方法实现者确保方法执行返回结果一定满足谓词P的要求,即确保P为真”。并列关系和或关系与前置相同。
- 副作用
副作用指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,从而给后续方法的执行带来影响。
JML提供了副作用约束子句,使用关键词assignable(表示可赋值)或者modifiable(可修改)。
- 异常处理
signals (***Exception e) expr
强调满足某个条件抛出相应异常,当 expr 为 true 时,方法会抛出括号中给出的相应异常e。
signals_only (***Exception e)
强调满足前置条件抛出相应异常。
JML工具链
OpenJML:
OpenJML
最基本的功能就是对JML
注释的完整性进行检查。检查包括经典的类型检查、变量可见性与可写性等。通过命令行使用OpenJML
时,可以通过-check
参数(缺省)指定类型检查。
JMLUnitNG:
根据JML规格自动生成测试并检查Java代码。
Junit:
单元测试,可以根据自己的测试要求自行编写测试程序,可以比较方便的测试自己的一些方法。由于是人工编写,显然实用性比上两种强。但由于自己编写测试时理解也可能与规格出现偏差,不能百分百保证没有bug。
二、针对Group接口实现自动生成测试用例
由于jdk版本和环境的原因,Openjml一直无法在电脑上运行,于是我从网络上找到了其他同学的部分生成数据进行分析。(图片来源:”圆*“ 的博客)
可以看到,JMLUnitNG的自动生成主要是测试一些边界条件,例如int的最大最小值、0、null,并对返回值进行检查。我认为这种程度的自动测试只能检测出一些语法错误,或是异常情况的处理错误,对于实现逻辑的问题就比较无能为力了,所以要想保证代码不出bug,还是需要自己手动编写测试代码。
三、梳理架构设计
第一次作业
第一次作业由于整体设计比较简单,也没有太多性能的要求,我选择了使用ArrayList作为容器。本次作业难点主要是isCircle方法,我使用了bfs遍历的方法来实现这个函数。整体代码构造是完全根据JML来写的,只不过把容器从数组换成了ArrayList。
第二次作业
第二次作业新增了Group类,由于本次作业有性能要求,所以我将容器换成了HashMap,其他大部分方法也是根据JML来编写的。对于性能要求比较高的RelationSum和ValueSum方法,我依旧按照JML采用双重循环遍历来算结果,但是为了提升性能,我采取了标志位的方法,当上一次请求和这一次请求的间隔中如果没有添加人或关系,就直接返回上一次计算出的值。
第三次作业
几个主要方法的复杂度如下:
第三次作业的主要难点是三个方法,求最短路径、判断强连接和求连通块数。对于求最短路径,我使用了比较朴素的Dijkstra算法,使用邻接矩阵来保存图。对于判断强连接,我也使用了比较暴力的算法,遍历所有节点,如果去除任何一个节点都不影响两点间的连通性,就判断是强连接。对于求连通块数,设初值为0,每次当加一个人的时候就让其加1;当添加关系时,如果添加关系的俩人原本是不连通的,添加之后变为连通的,就让其减1。
四、出现的bug和修复情况
第一次作业在强测和互测中均没有被发现bug。
第二次作业由于我还是采用了双循环计算,虽然采用了标志位的方法,但还是在互测中被hack了tle。同时我也发现了两位同学出现了同样tle的情形。在bug修复中我采取了每次加人的时候单循环更新Sum值,取值时直接返回。
第三次作业我在计算qsl的过程中手误将一个判断条件多判断了一个元素,导致一个极特殊的数据会出错。被一个细心的同学在互测中发现。在bug修复中将多余的if去除即可。
五、本单元学习的心得体会
在本单元的学习中,我一开始以为只是简单的照着JML编写代码,后来发现并不是如此。JML只是提供了一种规格要求,并没有强制性的要求什么样的实现算法。所以在实现中,不能一味的照搬JML,而是要自己思考应该用什么样的容器,什么样的算法,什么样的数据结构。经过本单元的学习,我学习了JML和契约式编程的有关知识,同时也复习了数据结构的知识,收获不少。