BUAA-OO-第三次博客作业-规格化设计
一、JML规格介绍
本章的三次作业均与JML规格有关,因此理解JML规格就成了必不可少的能力,我们先来看看JML的基础理论知识
JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言
(Behavior Interface Specification Language,BISL),基于Larch方法构建。
JML注释一般格式为//@annotation(行注释)或/* @ annotation @*/(块注释),一般包括三个部分
(1)前置条件 用requires子句限定
(2)副作用限定范围,用assignable列出这个方法能够修改的类成员属性
(3)后置条件 用ensures子句限定
JML注释常用表达式
\result 表示非void型方法的执行结果
\old 一个表达式在方法执行前的取值
\forall 全称量词
\exists 存在量词
JML类型规格
不变式invariant 要求在所有可见状态下都必须满足的特性
状态变化约束 constraint :对前序可见状态和当前可见状态的关系进行约束。
二、JML工具链准备
根据要求,在本次作业中首先我尝试部署了openjml,下载后
在命令行中运行 $ java -jar openjml.jar
将会出现openjml 命令的可选参数
后接想要检查的Java文件及其路径即可。
关于Jmlunitng的使用,参考讨论区的经验贴,在下载jmlunitng.jar文件后
首先运行指令 $java -jar jmlunitng.jar Demo/Demo.java ,生成测试文件
而后运行指令 $ javac -cp jmlunitng.jar Demo/**/**.java 编译生成的测试文件
运行 $ java -jar openjml.jar -rac Demo/Demo.java $ javac -cp jmlunitng.jar Demo/**.java 生成.class文件
执行 java -cp jmlunitng.jar demo/Demo_JML_Test 以自动测试我们的Java程序
运行结果如下(compare方法)
可以看到的是jmlunitng仅仅使用Integer.MAX_VALUE、Integer.MIN_VALUE和0进行检查,这对于我们第一次作业中的compareTo方法很有用,但如果对于其他方法,如此单一的测试点很可能导致问题无法检测到。
三、作业架构设计
第一次作业结构分析,类关系如下
在我的第一次作业中,只是按照相关要求建立了MyPath与MyPathConatiner两个类,并根据JML要求实现了相关方法,值得注意的是由于本次系列作业中对于时间复杂度的要求, 需要在完成JML要求的基础上,选择时间复杂度低的容器,并且把查询指令的时间复杂度分散到数量较小的图变更指令中,每次图变更时完成对于查询操作内容的计算,这样在进行查询操作时只是进行了读取操作,我的做法是维护三个hashset,分别是<结点--容器中该结点数量>,<路径--路径id>,<路径id--路径>,这样可以做到对于路径和路径id查询、删除时都具有o(1)的时间复杂度。由于需要将path作为hashmap的key,需要我们重写path的equals方法与hashcode方法。
方法复杂度分析如下
equals与compareTo的时间复杂度均达到了o(n),这样的计算结果也算是比较符合预期
第二次作业结构分析、类关系如下
在第二次作业中,由于新加了对于两点之间连通性判断以及求最短路径的功能,我的做法是在原来的基础上新加一个实现并查集的类一个描述边的类,这样可以用并查集来实现两点之间连通性的判断以及支持堆优化的dijksta算法,和第一次的作业一样,由于指令中对于图变更的指令相对较少,大量的是一些查询指令,因此依然将dijkstra的计算以及并查集的更新放在图变更指令中。
方法复杂度分析如下
可以看到复杂度主要集中在dijkstra算法中,但在使用堆优化的dijkstra后,复杂度有所降低(最后注释掉了dijkstra用了下面的dijkstraWithPri)
第三次作业结构分析、类关系如下
最后一次作业相比前两次,在难度以及算法的选择方面对我们的要求高了不少,其中最主要的就是对于最少票价以及最少不满意度的计算,这里我选择了dijkstra+拆点的做法,在第一次拿到作业指导书后,我虽然想到了拆点,但最初的想法是一个地铁站有几条路线经过就拆成几个点,并且让他们之间相互连通,但是这对于算法复杂度来说并不友好(相当于在图中加了一个最多有50个点的完全连通块),最后的结果也证明了我的想法,一组3000+条随机生成的指令就需要跑好几分钟,这对于强侧来说是灾难性的,在看了讨论区后果断选择了另一种拆点思路,才让时间复杂度得以控制。在构造方面,我新建了关于票价以及不满意度的两个图类(与之间计算最短路径的图十分相似,仅仅是在边权值方面有所变动),在图更改指令时加以维护即可。关于连通块的个数求解并查集数量即可,在此不再赘述。
类复杂度分析如下(由于方法过多,仅列出类的复杂度)
在方法复杂度中,复杂度高的方法几乎全是每个图类中的dijkstra,在类中也可以看到,拥有dijkstra方法的最短路径计算、最低票价计算、最少不满意度计算复杂度均较高
关于代码架构的重构与迭代
本单元的三次作业可以说是oo开始以来代码重构范围最小的一次,每一次新作业对于上一次实现的功能都十分友好,只要考虑新加入的功能即可,例如在第三次作业中,对于前两次作业已经实现的功能可以完全不予考虑(第二次作业bug修复完的情况下),只要考虑需要新加入哪些类以实现哪些功能即可,并且新加入的功能如最少票价以及最小不满意度相较于最短路径算法的变动都不算大,仅仅是在图构建时有微小区别。这样实现下来,每条指令所需要的各个功能之间几乎不会发生嵌套的情况,每个方法所对应的功能十分清晰,在笔者发现原先因为算法复杂度过高进行重构时,也仅仅修改了priceMap和unpleasantMap两个类中图构建的方法,相比前几次作业发现问题时定位修复所花费的时间可以说体会到了oop带来的红利。
bug修复情况
在第一次作业中对本章节对于算法复杂度的要求不能够做到十分理解,在第一次作业的计算不同节点数方法中,每调用一次方法都需要对容器中path进行一次遍历,导致时间复杂度过高,在助教修改时限的情况下最后两个点仍然超时。后改为Hashmap成功修复。
在第二次作业中由于在并查集中使用静态数组且在remove path后没有及时在并查集中删除相关节点,导致部分测试点RE,后在remove path的方法中添加对于并查集的更新解决。
规格化设计心得体会
经过三次作业对于JML语言的熟悉程度越来越好,也深刻体会到了规格化设计的方便之处,程序员只需要按照规格编写代码,保证好每个类中每个方法完全实现规格描述即可,减轻了程序员的工作量,但对于openjml+jmlunitng的自动化测试方法,个人在这里持保留意见(第一次使用的时候刷出来满满一篇报错),在网上也搜不到什么实用性的教程讲解之类的文章(全是同学们的博客)。可以看到的是,现在大家编程序仍然停留在每个人编写自己的程序,程序也不需要他人看得懂、也不需要实现良好的代码风格,但在今后走向工作岗位或是进行更深入的学习时,需要编写大型项目时,规格化的描述一定是一种非常高效的方式。