OO第三单元作业总结
一、前言:
本单元进行的是JML的学习。
二、梳理JML语言的理论基础、应用工具链情况
(一)JML语言的理论基础
1.概念:JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。
2.注释结构:JML以javadoc注释的方式来表示规格,每行都以@起头。有两种注释方式,行注释和块注释。举例如下:
行注释:
1 //@ public instance model non_null int[] nodes;
块注释:
1 /*@ requires index >= 0 && index < size(); 2 @ assignable \nothing; 3 @ ensures \result == nodes[index]; 4 @*/
3.JML表达式:JML的表达式是对Java表达式的扩展,新增了一些操作符和原子表达式。
(1)原子表达式:\result表达式、\old( expr )表达式、\not_assigned(x,y,...)表达式等。
(2)量化表达式:\forall表达式、\exists表达式、\sum表达式、\product表达式等。
(3)集合表达式:集合构造表达式。
(4)操作符:除Java语言所定义的操作符外,JML专门定义了子类型关系操作符、等价关系操作符、推理操作符、变量引用操作符四种。
4.方法规格
(1)前置条件(pre-condition),通过requires子句来表示。
(2)后置条件(post-condition),通过ensures子句来表示。
(3)副作用范围限定(side-effects),指方法在执行过程中会修改对象的属性数据或者类的静态成员数据,使用关键词 assignable 或者 modifiable。
(4)signals子句,结构为 signals (***Exception e) b_expr,意思是当 b_expr 为 true 时,方法会抛出括号中给出 的相应异常e。
(5)signals_only子句,后面跟着一个异常类型,满足前置条件时抛出相应的异常。
5.类型规格
(1)不变式(invariant)是要求在所有可见状态下都必须满足的特性,语法上定义invariant P,其中invariant为关键词, P为谓词。
(2)状态变化约束(constraint)对象的状态在变化时往往也许满足一些约束,这种约束本质上也是一种不变式。对前序可见状态和当前可见状态的关系进行约束。
(二)JML应用工具链
1.OpenJML:OpenJML是一个新的JML工具,包括RAC和ESC,与Java最新版本一同使用。OpenJML建立在Sun™的OpenJDK(javac的开源实现)之上。
2.JMLUnitNG:用来自动生成一个Java类文件测试的框架,结合OpenJML实现对代码的自动化测试。
3.JMLdoc:与Javadoc工具类似,不同的是它在生成的HTML格式文档中包含JML规范。
三、部署JMLUnitNG/JMLUnit
部署JMLUnitNG,首先安装OpenJML,按照伦佬的教程设置好相关环境后,进行用JMLUnitNG生成测试文件。首先先用以下demo文件进行测试:
1 // demo/Demo.java 2 package demo; 3 4 public class Demo { 5 /*@ public normal_behaviour 6 @ ensures \result == lhs - rhs; 7 */ 8 public static int compare(int lhs, int rhs) { 9 return lhs - rhs; 10 } 11 12 public static void main(String[] args) { 13 compare(114514,1919810); 14 } 15 }
执行
生成测试文件,接着利用以下两个命令进行编译
最后执行
产生了运行结果如上。其中Failed了三个样例,如图所示。说明我们的减法存在溢出。
(可能在JunitNG的使用上还有一点小的困难,我对Graph类的测试失败了。)
四、梳理作业架构设计
(一)第九次作业
本次作业最终需要实现一个路径管理系统。可以通过各类输入指令来进行数据的增删查改等交互。
类图:
分析:本次作业自己完成的部分是三个类,其中MyPath类继承Path类,MyPathContainer继承PathContainer类,Main类用来运行整个程序。
MyPath类中的内容:
分析:两个成员变量,Arraylist保存节点,Hashmap保存不同节点distinctnodes,方法主要有图中所示的获得大小(size)、获得序号为i的节点(getNode)等。
MyPathContainer中的内容:
分析:成员变量有保存各路径的pathList,保存路径id的pathid,保存整个容器中不同节点的distinctnodes,均用Hashmap实现。方法有添加路径(addPath)等。
其中,由于方法复杂度的限制,为了将distinctNodeCount方法的复杂度降低,选择在每次addPath和removePath(removePathById)时更改distinctnodes的内容,将结果保存下来,查询时不用遍历,从而避免了爆搜。
(二)第十次作业
类图:
分析:本次作业的类与上次作业相同的地方不赘述。不同的地方是
1.MyGraph类继承自Graph类,而Graph继承自上一次作业的类PathContainer。
2.新增了一个GraphContainer类,用于保存图的结构以及图的相关算法。
MyGraph类的内容为:
分析:与上次作业相比新增了containsNode、containsEdge、isConnected、getShortestPathLength四个与图结构紧密相关的算法,重点是如何建立起图的体系,这一点我放在新增图类MyGraphContainer中实现,Graph类包含一个MyGraphContainer类的成员变量,用于查询与图有关的各类信息。
MyGraphContainer类的内容如下:
分析:
1.图结构的存储表现形式为邻接表,采用一个略微复杂的二级Hashmap具体实现。其中,一个节点的id作为key,索引到另一个Hashmap,建立起每个节点与其他所有节点的一个网络,最终的hashvalue体现二者之间是否存在Edge。
2.最短路径寻找使用BFS广度优先搜索法,这个算法大同小异,因此不再过多赘述。值得一提的是,由于我找到的算法版本较强化,会将最短路径所经过的各个节点都存储下来,然而我们只需要最短路径长度这一数值,因此增加了复杂度,导致CPUtime过长。
3.为了降低查询最短路径长度时的复杂度,仍采用一个二级Hashmap结构存储某两个节点之间的最短路径。每次addPath和remove的时候,更改此表中内容,保存结果以便查询。
(三)第十一次作业
类图:
分析:这次作业的类之间关系几乎与上次作业完全相同,唯一的更改是MyRailwaySystem类继承自RailwaySystem类,而RailwaySystem继承自上次的Graph类。
MyRailwaySystem类的内容:
分析:与上次相比,新增了查询最小票价、查询最少换乘、查询最小不满意度、查询连通块个数四个方法。同样的,将图结构存储和具体的实现算法保存在自己的RailwayGraph类中,此类作为一个成员变量存在于MyRailwaySystem类,从而获得相关的信息。
RailwayGraph类的内容:
分析:
1.用二维数组这一容器建立多个以邻接矩阵为实现形式的图,保存着各个节点的信息,由于算法的需要,这些图的结构大同小异,主要的不同是边的权值,设置不同的权值来体现最少换乘、最低票价以及最少不满意度。
2.计算加权最短路径的算法是Floyd算法,其好处是,(1)代码简洁(2)算法完毕后邻接矩阵直接变为最终用来查询的图。(3)一次空间上的遍历可完成多个图的计算,某种程度上减少了遍历的次数。
3.计算连通块采用dfs深度优先染色算法。
五、分析代码实现的bug和修复情况
(一)第九次作业
进行removePath时,用foreach进行遍历,判断equal后直接在for循环中使用Hashmap的remove将其删去,造成for循环的不稳定,产生了某些语句没有输出的情况。如下图:
修复方法:采用temp存储,找出equal的path后break,for循环结束后利用temp找到应该remove的内容,进行删除操作。如下图:
强测中,因为这个bug,wa了18个点,只得了10分,没有进入互测,修复后即通过了所有强测点。
(二)第十次作业
三个bug。
1.删边的时候没有考虑到无向图的对称性,导致只remove了“一条边”。如下图:
修复方法:
2.CPUtimelimit。正如上文所说,计算最短路径的算法BFS时间较久。
修复方法:换了算法和结构,用邻接矩阵和Floyd算法。
3.对规格没有仔细研究,当两个节点相等的时候,最短路径应该是0。
(三)第十一次作业
删除路径的时候,我将所有的图推翻重构,此时对distinctnodes中内容的改变出现漏洞,造成bug。
修复方法:遍历更新一遍distinctnodes。
六、对规格撰写和理解上的心得体会
对于规格的撰写,我个人有以下几点心得体会。1.规格撰写的时候逻辑一定要严谨,考虑到各种复杂逻辑下的边界情况,才能写出严密的规格。2.规格的撰写要符合语法,这样才能具有更高的可读性。3.规格的撰写有助于规约我们写代码时候的逻辑,使我们的代码单元化,封装化,更易于代码的维护。
对于规格的理解,我认为首先应该熟悉JML的语法,其次,应该保证代码的行为完全符合规格的规定。JML的使用有以下好处:能更加精确地描述代码所完成的任务,有效地发现和纠正错误,减少随着应用程序的进展而引入错误的机会。所以,学习JML还是很有必要的。