OO第三单元作业总结
一、JML语言
1、理论基础
JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。
一般而言,JML有两种主要的用法:
(1)开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。
(2)针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。
在本单元,我们主要学习了以下几点:
JML表达式:原子表达式如\result、\old;
量化表达式如\forall、\exists;
集合表达式;
操作符如推理操作符==>和价关系操作符<==>。
方法规格: 分为正常行为规格和异常行为规格
每一种规格都可以设置前置条件、后置条件和副作用范围限定(这也是方法规格的核心内容)。
类型规格: 不变式限制(invariant)和约束限制 (constraints)
不变式(invariant)是要求在所有可见状态下都必须满足的特性。
约束限制(constraint)来对前序可见状态和当前可见状态的关系进行约束。
2、应用工具链
JMLUnitNG:可以自动生成测试数据来检验代码的正确性。
SMT Solver:用于验证代码和规格的等价性。
OpenJml:检查JML的规范性。
…………
本单元作业对这几种工具都有一定的简单尝试。
二、部署JMLUnitNG,实现自动生成测试用例
配置过程参考讨论区https://course.buaaoo.top/assignment/71/discussion/199
自己作业的代码实现数据类型与接口不一致,修改规格也比较困难,因此还是参照讨论区,实现了一个简单的类进行简单测试,测试类的代码如下:
1 public class Main { 2 public static void main(String[] args) { 3 4 } 5 6 /*@ public normal_behaviour 7 @ ensures \result == x - y; 8 */ 9 public static int sub(int x, int y) { 10 return x - y; 11 } 12 13 /*@ public normal_behaviour 14 @ ensures \result == x / y; 15 */ 16 public static int div(int x, int y) { 17 return x / y; 18 } 19 }
测试步骤:
java -jar jmlunitng.jar Main.java javac -cp jmlunitng.jar *.java java -jar openjml.jar -rac Main.java java -cp jmlunitng.jar Main_JML_Test
得到结果:
测试结果分析:
发现了bug,存在除0和溢出问题。通过观察可以看出以上自动生成的测试数据,都是在数据范围边界的数据,0比较特殊,介于正负数之间,-2147483648和2147483647为int的最小值和最大值。生成的测试数据比较极端,可以找出边界条件处的bug。
三、架构设计
第一次作业:
第一次作业比较简单,基本上只按照了指导书的说明进行设计,重点放在了优化上。
这一单元测试有了时间的限制,我也是第一次面对运行时间的问题,怕自己以前惯用的数组超时,因此学习了hashmap等相关容器的用法(还真好用啊),着重对时间的考虑。
在存path时使用了hashmap,考虑到删除路径时可以通过path和对应id删除,path索引id,id也应索引path,因此用了两个hashmap对称存储path及其id,避免删除路径时遍历查找hashmap,节省时间。
在path中用hashset来保存不同节点的个数,而在PathContainer中用hashmap用来存节点及其存在的次数。
在创建path对象时就算出了其hashcode并存起来,以免以后使用时再算一遍。
第二次作业:
第二次作业基本上延续了第一次作业的构架,MyPath类基本没改。
由于第二次作业要将各个path连成图,我使用了最容易想到也最直观的邻接矩阵。在实现上,将int范围内的数映射到1-250之间(最多同时存在250个不同的点),其实就是下标(1-250)对应着int范围内的数,这样保存起来一个映射关系,然后每输入删除一个path就对矩阵进行操作。也许是我的方法比较蠢,全都是用静态数组,因此实现映射并操作矩阵时复杂度较高,也比较难理解,自己都快绕晕了。应该可以用别的容器(比如hashmap)来存对应关系,可能会降低一些复杂度。
每次添加或删除路径,就用floyd算法算一遍距离并储存到距离矩阵,而不是在有查找指令时再去算。
containsEdge可以通过邻接矩阵直接得到,isConnected和最短距离可以通过距离矩阵直接得到。
第三次作业:
第三次作业也基本上延续了第二次作业,不过差不多写了个超级类出来(很面向过程)。
本来一开始打算用拆点法,挺好理解,但是具体实现起来感觉很困难。wjy大佬在讨论区分享了不拆点的做法,实现简单,而且具体实现框架跟我第二次作业比较相近,于是乎就用了。基本上之前写的代码都没有变,而且新增的功能跟之前的某些功能实现方式很类似,稍微梳理一下的话实现起来还是比较容易。
这种做法的话需要计算出每条path内的任意两点的不满意度和最短距离,我延用了floyd算法实现,然后根据该path的矩阵更新总体矩阵,最后再将总体矩阵floyd走一遍,得到两点间的最小不满意度和最低票价,也就是增加一条路径要进行两次floyd操作(一次floyd会对多个不同的功能矩阵同时进行操作),第一次floyd算出path的信息,第二次floyd算出图的所有信息。
四、代码实现的bug(?)和修复情况
本单元第一次的作业中存在着一个bug,就是equals方法只比较了哈希值,认为哈希值相同两个对象就相同,当时也知道了存在这个问题,但是觉得找不出来这样哈希值相同但内容不同的对象,偷懒就没有改,结果互测中被找出来了,还是低估了同学们找bug和造数据的能力。修复就是哈希值相同的话就逐一比较每个元素,得到正确结果。
第二次作业和第三次作业均未出现bug。不过在第三次作业中,其实有tle的风险,因为我在实现的remove方法时没有考虑复杂度,简单将容器中剩下的path路径全都floyd算一遍局部最低不满意度和局部最小距离并更新总体矩阵,其实就是相当于从0开始add了所有剩余的path。这样的话50条add和remove指令极限情况(先34条add,再16条remove)相当于大约500条add,我在本地测试大约要用6、7秒的时间,因此还是有点慌,不过没时间改了也就没有改,好在最后没有问题。我能想到的对这个问题的简单优化就是将算出的这些path的局部功能矩阵存起来,避免下次使用时再重新算。
五、对于规格的心得体会
规格,正如老师课上所说的,是一种契约,这种契约是需求者和代码实现者之间的契约,有了这种规格的说明,使需求者能够得到正确的返回结果而不去关心具体实现细节并且没有歧义,而使实现者不必拘泥于给定的实现方式而可以根据自己的理解以自己的方式实现,双方都遵守这个契约,那么项目代码的正确性与规范性就可以得到保证。
在这一单元中,我们是根据已有的规格来实现相应的方法功能,这种规格说明是没有二义性的,有些简单规格含义比较清晰,但是某些规格的描述十分复杂(因为其采用的数据结构),这就给读者带来了很大的理解上的麻烦。特别是我们这次的作业,指导书给出的规格中使用的数据结构和我们自己实际实现的数据结构几乎根本不同,因此阅读规格实在是痛苦。因此我在阅读规格时就偷了懒,基本只读了前置条件和后置条件就去完成功能去了,指导书描述的功能也比较明确,因此没有出错。
其实比起根据规格写代码,我对写规格更感兴趣,如何写一个完备、正确而又简洁的规格真的需要用心琢磨,当然课上上机也对规格撰写有了简单的练习,但是我想既然这一单元是对规格的学习,那么课下作业出一个写规格的任务而不全是根据规格写代码可能会更好吧。