OO第三单元总结——JML规格

JML语言

JML语言是Java程序的一种规格语言,其主要对接口行为、功能和规格作出描述和规定,能够有效确保设计者的本意被清晰表达。

方法规格是JML语言中最重要的一部分之一,包括前置条件 precondition,后置条件 postcondition,副作用范围限定等。前置条件主要利用 requires 语句描述,调用者在调用时需要确保 requires 后跟着的表达式为真。后置条件主要利用 ensures 语句描述,调用者同样应该确保在方法执行后返回结果满足 ensures 后跟着的表达式为真。assignablemodifiable 关键词可以用来限定副作用的范围,方法执行过程中会修改的对象属性数据或类的静态成员数据,必须通过副作用约束语句显式地声明出来。

JML语言的工具链目前也有很多,比如OpenJML可以用来检查JML的规范性,JMLUnitNG可以用来自动化生成测试数据,SMTSolver可以用来检查代码等价性等。当然,还有更多工具等待我去发掘。

JMLUnit

架构设计

  • 第一次作业

    第一次作业要求实现的功能较简单,大部分都是查找类的指令,因此需要注意的部分主要是对存储容器的选择,考虑到时间复杂度,选择HashMap是很自然的事。

    我在MyPathContainer中采取的存储策略是用path的id作为Hashmap的key,由于每条不同path的id都不会相同,因此这种策略不会造成重复存储或错误存储等问题,PATH_GET_BY_IDCONTAINS_PATH_ID 的时间复杂度只有O(1),但对于 PATH_GETCONTAINS_PATH 这种根据path查找的指令,时间复杂度还是有O(n),虽说仍在可控范围内,但还是不尽完善。

    还需要注意的指令是 DISTINCT_NODE_COUNT ,若是每次收到这条指令均进行计算的话超时不可避免,因此需要将这部分的计算工作分散到其他指令中。由于只有path增加或删除时会改变不同节点数,因此在每次path增删类指令中计算不同节点数,并将结果作为MyPathContainer中的属性保存下来即可。

  • 第二次作业

    第二次作业主要增加了求解两个节点之间最短距离的指令。与上一次相比,本次的架构需要加上存储图的结构。我采用的是用静态数组构建邻接矩阵存储,因此还需要HashMap存储每个节点和其在邻接矩阵中对应下标的映射关系。上一次作业留下的架构倒不需要变动,因为查找等指令仍然存在,但在path增删类指令上面需要加上对本次图结构变动的支持。

    采用静态数组存储邻接矩阵带来的问题就是,删除一条path时将十分棘手。本次作业我也是在这里出现了一个Bug,之后会详细分析。删除一条path时,需要对path中的每一条边都进行删除,并且如果有节点在其他path中均未出现,其在邻接矩阵中的行和列都需要删除清空。为了在改变邻接矩阵结构时改动最小,我在把一个节点从邻接矩阵中删除时,先将矩阵中最后一个节点的行和列与该节点交换,再进行删除,这么做只需要改动两组行列,避免了将要删除的节点之后的节点全部前移的麻烦。

    对于求节点是否连通以及最短路径,我采用的是Floyd方法,实现起来简单,复杂度是 。和上次作业一样,我在path增删类指令之后都会进行一次Floyd算法,将结果保存下来,收到最短路径和是否连通的指令时直接访问经过Floyd算法的最短路径矩阵即可,不需要再进行额外计算。这样做可以极大地减少CPU时间,在强测中CPU时间最多的点也只有4秒多而已。

  • 第三次作业

    第三次作业主要增加了求最少换乘、最少票价以及最少不满意度的指令。与第二次作业相比,由于增加了不同的需求,因此需要多个图结构来帮助我们分别求解换乘、票价和不满意度,每个图按照要求设置边的权值,对每种指令访问其相应的图结构便可得到结果。本次作业的架构是在第二次作业架构的基础上新增了三个图结构,分别有不同的权值来存储求最少换乘、最少票价和最少不满意度,并且增加了二维HashMap,用来存储每条边在每个path中的信息,如距离与不满意度。

    采用多个图存储不同信息的优点是直观容易理解,可以对不同的图采用同一个算法求解,本次作业我使用的是Dijskra算法,但同时在图的维护上也需要很多额外工作。Dijskra算法的实现过程中要注意对换乘的处理,对最短路径,换乘不需要考虑;对最少换乘数,每换乘一次结果都要加1;对最少票价,需要加2;而对最少不满意度,要加32。这里要感谢同学们在讨论区里的分享,拆点法我也研究过,但感觉理解得不是那么透彻,没有选择拆点法。

    第三次作业主要困难的地方在于对不同图结构的变动。由于图结构很多,在path增删指令之后对所有的图都要进行更新,写的不够仔细的话很容易出现错误,不幸的是我在删除节点改变图的地方再次出现Bug,强测勉强存活,但也仅仅是勉强而已。

    这次作业还有一条指令是求连通块的数目。这个实现起来倒比较简单,依照标准的dfs算法实现即可,不需要对架构进行任何改动。

Bug分析

第一次作业没有出现bug。

第二次作业中有一个bug,在删除节点对邻接矩阵进行处理时,交换矩阵中的元素时多了一个循环,导致本该被清0的点实际值不为0,产生了错误。将循环数减少一次之后修复完毕。

第三次作业中有一个bug,同样出现在删除节点处,由于我使用了二维HashMap存储每条边的信息,在将同一个节点之间的边对应的HashMap进行交换前先置成了null,导致如果再次访问这个HashMap会报错。将交换与清空成null的顺序交换便可修复完毕。

心得体会

其实现在仔细想想,写三次作业时我对程序规格的阅读远远不够仔细,每次作业都是过一遍就罢,毕竟指令本身的名称表述的含义就已经足够清晰。现在再回去看规格,才发现原来有很多地方,包括各种特殊情况,规格都有指出,若是认真根据规格编码应该会少走很多弯路。但同时,由于本单元作业每个人的架构都不尽相同,根据给出的规格写完方法后并不足够,还要考虑到程序本身的架构实现其他方法辅助我们完成规定的任务,这样一来难免会出错。现在想来,如果当时在编写自己的方法之前也撰写规格,是否能避免各种bug的出现呢?

不可否认的是,规格的撰写的确能够在很大程度上规范我们对于方法的编写,在几次上机的过程中也可以发现清晰简洁的规格对于编写代码的速度和完备性都有明显提升。理想的规格可以使代码编写、测试、管理都更加高效,且突出了模块化编程、工程化编程的思想。

最后一个单元了,坚持下去,走完剩下的OO之路。

猜你喜欢

转载自www.cnblogs.com/daytona2018/p/10891504.html
今日推荐