北京航空航天大学2019年OO课程第三次总结

0.前言

这已经是第三次OO课程的总结博客了,由于本单元侧重于规格,因此总结博客的内容和前两次总结博客有较大差别。本次博客中,会涉及JML的相关知识和操作,我的JML学得并不太好,正好借着本次机会加强学习。

1.JML基础

1.1.JML理论基础

JML(java modeling language)即Java建模语言,用于对Java程序进行规格化设计的一种表示语言,可以用来描述一段代码的具体行为,比如前置条件、副作用、后置条件等。面对对象的分析和设计的一个原则就是过程性的思考应该尽可能地推迟。Java建模语言在Java代码中增加了一些符号,这些符号用来标识一个方法是干什么的,却不关心它的实现。通过这种方式,JML把过程性的思考延迟到了方法设计中,从而拓展了面对对象的这个原则,同时通过引入大量用于描述行为的结构,比如有模型域、量词、断言可视范围、预处理、后处理、正常行为与异常行为等等。通过JML的相关支持工具,可以检查规格是否合乎规范、可以基于规格自动构造测试用例,同时可使用SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。

JML主要的规格如下所示:

表达式

  • \result表达式:表示方法执行后的返回值。

  • \old( expr )表达式:表示一个表达式expr 在相应方法执行前的取值。

  • \forall表达式:即全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。

  • \exists表达式:即存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。

  • \sum表达式:表示返回给定范围内的表达式的和。

  • \max,\min表达式:表示返回给定范围内的表达式的最大值、最小值。

  • 等价关系操作符: b_expr1<==>b_expr2 或者b_expr1<=!=>b_expr2 分别表示表达式相等或不等。

  • 推理操作符: b_expr1 = => b_expr2 或者b_expr2 <==b_expr1 。

方法规格

  • 前置条件:requires ,对方法输入参数的限制。

  • 后置条件:ensures ,对方法执行结果的限制。

  • 副作用范围限定: assignable,可赋值;modifiable,可修改

  • pure方法:纯粹访问性的方法,即不会对对象的状态进行任何改变,也不需要提供输入参数。

  • 方法的异常行为:normal_behavior, also, exceptional_behavior, signals (***Exception e) b_expr, signals_only。

类型规格

  • 不变式 invariant,要求在所有可见状态下都必须满足的特性。

  • 状态变化约束 constraint,对前序可见状态和当前可见状态的关系进行约束。

1.2.JML应用工具链

我所使用的JML应用工具链有以下几种:

OpenJML:通过在IntelliJ中安装OpenJML插件,理论上可以调用SMT solver进行规格的检查,但是由于openjml可能还不支持对\exists和\forall表达式的分析和验证问题,因此无法使用。

JMLUnit:通过在IntelliJ中配置相应jar包,自动生成测试样例测试规格的正确性。 

2.JMLUnit测试Graph接口实现

折腾了不少时间,仍然没搞明白,因此只是采用了最简单的实现。

第一步,编写测试类

第二步,安装openjml和jmlunitng

 

第三步,测试

1.执行命令 jmlunitng demo/Demo.java 生成测试文件

2.执行命令 javac -cp jmlunitng.jar demo/*.java

      openjml -rac demo/Demo.java 编译

3.执行命令 java -cp jmlunitng.jar demo.Demo_JML_Test 运行测试文件

运行结果如下

从运行结果可以看出,程序通过了测试,且生成的测试数据都是边界数据,完全满足测试需求。

3.架构设计梳理

本单元的作业偏向于规格设计,因此需要干什么,规格其实比指导书说的更加清楚。本单元的作业的难点是选取恰当的数据结构和方法,并维护增删路劲以后程序的正确性,再次基础上考虑代码的时间复杂度问题。因为仅仅完成功能是不够的,因为效率太低是无法满足作业的时间要求的。

第一次作业中,需要完成的任务为实现两个容器类Path和PathContainer。通过分析本次作业需要实现的功能,在本次作业中并不涉及复杂的方法。唯一的难点就是实现获取容器内不同结点个数的功能。但是这个功能只需要选取合适的数据结构就能解决。最初我想使用Hashset,Hashset是一种特殊的Hashmap结构,能够自动过滤掉相同的点,因此只需要无脑将每条path的所有点遍历加入Hashset中即可,但是这样却忽略了一个问题,即删除path时,Hashset中的点的删除不能实现,并不知道Hashset中的点能不能删。最终还是选择了Hashmap,value中存对应点的出现次数,删除path时减少对应点的个数,当个数为0是,从Hashmap中删除该点,获得个数只需要直接get Hashmap的size即可。

第二次作业中,Graph接口中继承了PathContainer的多数方法(只有和新方法有关的需要重新实现,例如addPath和removePath)不需要重新写,直接复用MyContainer中的代码即可。新增加的四个功能中,判断容器中是否存在某个结点不需要另外实现,因为第一次作业中实现的pointCount的Hashmap中已经存储了所有不同的结点,只需要判断节点在不在pointCount中即可。剩下的三个功能,容器中是否存在某一条边,两个结点是否连通,两个结点之间的最短路径长度,在我看来都是一个问题。这三个功能其实是在要求我们实现一个存储了所有点直接最短路径长度的数据结构(如果两个结点之间没有路径,则最短路径长度设置为无穷大)并维护它。有了这个图以后,,是否存在某条边,只需要判断这条边对应的端点之间的最短路径是否为一,为一则便存在,否则不存在;判断两个结点直接是否连通,只需要判断两个结点之间的最短路径是否小于无穷大,小于则两点连通,否则两点不连通;两个结点之间的最短路径长度只需要直接从图中取出就行。我使用了一个Hashmap的嵌套来模拟二维数组,外层Hashmap中key值存第一个结点,value中存内层Hashmap,内层Hashmap key值存另一个结点,value中存最短路径长度。求最短路径长度我使用Floyd算法,这个算法很简单,就是插点法。一共三层循环,有点A,B,遍历所有点找点C,点C分别到A,B的最短路径之和大于A,B之间的最短路径长度,则替换。本次作业的难点就是Floyd之前的准备以及增删path之后的维护。

第三次作业中,新增的功能有求整个图中连通块的数量,两个结点之间的最低票价,两个结点之间的最少换乘次数,两个结点之间的最少不满意度。其中,连通块可以直接使用并查集。后面三个功能我最初没有头绪,后来在讨论区知道了分点法,可是有多问题我都没法处理,或者很麻烦,肯定在ddl之前完成不了。后来看到了大佬发的给图的边赋上不同的权值然后用然后用类似求最短路径长度的方法算出,需要维护类似最短路径长度的图。这次的作业主要是换乘的问题,这种方法保证了在每条路径里,都有一条边直达换乘点,每条路径都会算一次换乘次数,最后扣除第一条路径的换乘就完成了。

三次作业中,我每次都没有直接继承上一次的容器,因为这样继承会出问题。在本单元的作业中,我发现我对于继承的理解还不够。如果MyGraph继承MyPathContainer,还是得重写所有代码,包括完全一致,可以复用的方法,因为总是有方法是需要重写的,没有重写的方法会把数据存到MyPathContainer中去,重写的方法却会访问MyGraph里的数据,从而引发bug。恰当的处理能够解决这个问题,但是却十分麻烦,不如不继承。

为了降低时间复杂度,将很多图的维护都放到了addPath和removePath中,这也是因为addPath和removePath出现的次数有限。

4.bug分析与修复

 第一次作业没有什么坑点,但是我翻车了,没有进入互测。最初一直想不通是什么问题,直到强测结果出了以后才找到问题,我的强测只得了25分,通过样例分析知道是由于我的path比较算法有问题导致的,我翻了大一新生都不会犯的字典序比较的错误。

第二次作业进入了互测,但是只得了55分,这次并不是有什么bug,而是我由于时间关系,没有删除已经删除的path的对应点,导致整个图太大,运行超时。修复bug时实现了无用结点的删除机制就成功的修复了bug。

第三次作业完成的比较晚,代码风格的问题也很大,由于时间关系,匆匆忙忙修改完代码风格就已经到时间了,结束前用一个大数据样例跑出了大量异常,已经来不及修复了。   

5.心得体会

本次的作业的重点就是两方面:一方面是学习理解规格,能够自己根据规格写代码,也能够自己写规格测试代码;另一方面则是由本单元左右的性能要求导致的,必须要选用恰当的数据结构和运算方法,来减少时间复杂度。选用恰当的方法做事能够事半功倍。另外,自己测试的时候一定要测试充分,否则强测会死的很难看。一定要给自己留出充分的时间来完成作业。

posted @ 2019-05-22 20:22  王焜  阅读(214)  评论(0编辑  收藏  举报