目录
- JML 的理论基础和应用工具链
- SMT 验证
- JMLUnitNG 测试
- 架构设计
- Bug 分析
- 心得体会
一、JML 的理论基础和应用工具链:
理论基础:
- JML 是一种基于 Larch 方法构建的行为接口规格语言。通过类 Java 的语法和近似数学公式的语言来描述类型(Type)(包括类和接口)、方法的不变约束、前置和后置条件。这种形式化的语言不仅没有自然语言产生的二义性,同时也便于使用工具做代码等价性检验。
- JML 中几个比较重要的概念分别是:
- 前置条件:描述输入参数必须满足的限制,如果不满足,将导致异常行为。
- 后置条件:描述输入参数满足前置条件的前提下,方法执行得到的“正确”结果。
- 副作用范围限定:描述在方法执行过程中可以被修改的域(实例域和静态域)。
- 不变式:表示变量在可见状态下的取值空间。
- 状态变化约束:表示变量在方法执行前后所能够变化的程度。
- 在规格的撰写者和程序的实现者都充分理解了 JML 的前提下,JML 规格可以没有二义性地约束类的状态空间和方法的执行后果,帮助架构师指导程序员实现他所期望的功能。但同其他规格一样,JML 并不关心功能的具体实现,这给实现者留下了一定的灵活度。
应用工具链:
- 由于 JML 在工业界并没有被广泛接受,JML 的应用工具链不是很完善。
- 主要有:
- OpenJML:基于 JML 规范实现的 JML 检查工具,可以进行基本的 JML 语法检查,在搭载了 SMT Solver 后可以对代码进行形式化验证,检查代码的实现是否满足规格要求(支持静态和动态检查)。
- JMLUnitNG:根据代码中的 JML 规格,自动生成测试样例的工具。
二、SMT 验证:
- 由于我的实现模型跟规格所要求的并不完全一致(主要是 assignable 部分的问题),因此在 SMT 检查时出现了错误信息:
三、JMLUnitNG 测试:
这三个 Project 的代码由于不知名的原因无法生成测试文件,改用其他代码进行生成。
改用自己写的代码进行测试:
public class MyTest { //@ ensures a > b ==> \result > 0; //@ ensures a < b ==> \result < 0; //@ ensures a == b ==> \result == 0; public int compare(int a, int b) { return Integer.compare(a, b); } }
测试结果截图:
四、架构设计:
由于三次作业架构上变化并不是很大,因此我只放出第三次作业的 UML 类图,避免文章过于冗长。
第一次作业:
- 第一次作业由于需求比较简单,因此直接按照 JML 规格给出的要求实现就基本完成了,并没有什么很特别的架构。
- 值得注意的是,由于会有很多次查询
Path
是否存在于PathContainer
中,因此直接使用ArrayList
之类的查询时间为\(O(n)\)容器是肯定会超时的,在此我使用了TreeMap
作为存储Path
的容器,避免超时。 - 同时由于接口中有
getDistinctNodeCount()
这个查询不同点的方法,如果每次查询都需要遍历一次容器也肯定会超时,在此我使用了缓存的思想:每次添加、删减Path
的时候都更新distinctNodeCount
,避免每次查询都需要遍历。
第二次作业:
- 第二次作业中与新增需求无关的部分全部沿用第一次作业的代码。
- 第二次作业由于增加了最短路计算和边的删减等图论问题,因此我单独构造了一个无权图类
UndGraph
(非下发的Graph
接口),支持:增加、删减边;计算所有点对的最短路径;获取两点间的最短路;检查是否包含边;检查是否包含路径等操作。 - 这个图类并没有兼容第三次的要求。其实,我已经考虑到了第三次作业可能会加入带权边的需求,但是为了能够在第二次作业中使用 BFS 算法(不支持带权最短路),我就没有加入对带权最短路的支持了。
- 实现了
Model\Graph
接口的MyGraph
接口通过组合UndGraph
对象实现与图论相关的需求。
第三次作业:
- 第三次作业新增了带权图的需求,这部分需求我通过一个带权图类来实现。
- 但这个带权图类并非继承自第二次作业的无权图类,而是另起炉灶写了带权图类,同时修改了无权图类,这两个类的耦合度很高:在无权图类组合了 3 个带权图类的对象,把票价、不满意度、换乘等查询交给带权图类对象完成,(请见 UML 类图),这个地方不太OO,也是我自己不太满意的地方。
- UML 类图:
五、Bug 分析:
- 本单元的三次作业强测都是 100 分,互测中也没有被发现 Bug 。课下自己测试的时候只有第三次出现了 1 个小 Bug 和 TLE 的问题。
- 第三次作业的小 Bug :带权图中获取两点间最短路时没有考虑
fromNode == toNode
的情况,是写代码的时候不够细心导致的。 - TLE 的问题:带权图类沿用了之前无权图类使用的容器
TreeMap
来存储数据,导致时间复杂度中的常数过大,自己测试的时候发现超时了。
六、心得体会:
- 为什么要撰写规格:具体的工程项目实现时是有分工的,并不可能一个人从头到尾全部完成,因此完成不同部分的人需要统一接口,这就需要规格来进行约束。同时,工程项目的顶层设计是由架构师来完成,程序员负责具体实现的,这也需要规格来进行交流。
从撰写规格的原因就可以看出来,撰写的规格必须具备两个特性:通俗易懂和无二义性。通俗易懂能够避免解读规格所需的时间,提高程序员实现的效率。无二义性能够避免规格被错误解读,避免程序的实现与设想不符。
同时,规格只约束了方法的功能、变量的取值空间,却没有限制方法、变量的具体实现,这就给实现者留下了一定的灵活性,能让他们按照自己的习惯实现程序,在效率和可读性、可维护性上进行有考虑地折中。