OO第三单元总结
一、JML基础及工具链
JML基础
jml是用于对Java程序进行规格化设计的一种表示语言。简单地说,规格就是设计者、开发人员与用户的“三方协议”。设计者根据用户需求来进行抽象,用形式化的语言来描述用户的需求,而开发人员则将其进行具体的实现。而本单元所用的jml就是具有这种功能的语言之一。对于jml,应该注意以下几个方面:
表达式:要想表述出具体的意思,就得靠表达式。常见的表达式有原子表达式和量化表达式等。部分常见表达式如下:
\result:表示返回值
\old(expr):表示表达式expr在相应方法执行前的取值。通常情况下建议把想要表达旧值的部分整个放在\old中
\not_assigned(x,y,...):如果括号中的变量在执行中未被赋值,则返回true,否则返回false
\forall:全称量词修饰的表达式,与我们在离散中学习的类似
\exists:存在量词修饰的表达式
\sum:返回给定范围内的表达式的和
方法规格:个人认为这是jml最重要的内容。其主要内容包括:
前置条件:要求调用者必须满足的条件,只有满足条件该方法才能被执行。
后置条件:即方法执行完成后应该满足的条件。
副作用:方法执行过程中可能会修改对象的属性数据等,而能够或不能够修改的部分在这里指出
除上述之外,方法规格中还可以指定某些条件下抛出的异常。
类型规格:类型规格主要制定成员变量的变化规则。也就是说,成员变量的改变必须要满足其限制。
工具链
JML的工具有很多(但貌似都不是很完善),列举部分如下:
openjml:可以检查jml规范性、静态检查程序可能的问题等
SMT Solver:检查代码与规格的等价性
JMLUnitNG:可以用来自动生成测试数据
二、工具链的使用
话先说在前面,体验极差!
本来应该时用于测试主要函数,但是openjml实在太逊,我自己之前写的一些简单的测试用的类也没有得到理想的结果,更不用说这些复杂的类和函数(而且一测就是一堆不知所以但应该没用的信息),因此我把MyPerson类拉出来重写了一下并用其进行测试,算是起到一个示范作用。
新生的Person类完全(且仅仅)实现了原Person接口中的方法,其中acquaintance采用数组的形式,value采用ArrayList的形式,结果如下(本来有很多的,为了简洁只列出一些):
由此可见openjml确实不太行,采用不同的数据结构实现其就无法判断。
本来想采用JMLUnitNG对MyGroup进行测试,但它无论如何也跑不起来,又是建议升级编译器又是警告无法找到注释方法啥的,换jdk改环境变量啥的都不行,也找不到解决方法。然后我参考了一下往年的博客,抱着试一试的心态尝试了一下里面给的测试方法,发现居然没出啥问题……于是没办法,只能用刚刚这个Person来做测试。这个类在刚才的基础上被加上了main方法,结果如下:
可以看出这东西几乎没啥用。结合别人的使用经验来看来看它主要测边界数据,而且貌似只能测一些特定的类型,一顿操作猛如虎再看战绩零点五毫无疑问说的就是它了。
PS:完成这一步的过程中我发现即使试别人的可以正常跑,但我自己写的Person一开始是不能正常跑的。敲完指令以后没有任何报错,也不生成任何文件,让我感到很奇怪……最后我把构造器删了它才能正常生成文件,也不知道什么原因……总之体验极差!
三、作业分析
第一次作业
这次作业我就是完全照着规格写的,非常稳。虽然课上说过注意架构,但是我觉得这次貌似真没什么必要,对着规格写就完全没问题。实现规格的时候容器几乎全用的是ArrayList。这次作业唯一一个难点就是isCircle,我直接采用了bfs的方法,顺利通过第一次作业。
由于这次作业较为简单,互测中既没有hack别人也没有被hack。
第二次作业
这次作业中加入了分组的概念,并对组进行一些操作。考虑到我第一次的性能实在太差,本次我采纳了群友的建议,用了ArrayList+HashMap的方法。ArrayList用于遍历,HashMap用于查找。由于并没有删减的操作,因此这种方法还是比较合理的。
本次Group里的方法有许多按规格来得用双循环,但实现的时候双循环必定会超时。考虑到只有在每次加人(或者link)的时候ageMean、ageVar等才会变化,因此采用缓存机制即可有效降低复杂度。这里ageVar需要用一点数学变换,同时计算一下可以知道年龄平方和并不会溢出,因此用int就行。
这次作业也不难,强测互测也未被发现bug。互测的时候发现有一位双循环的铁憨憨,于是成功让他超时了。
第三次作业
这次作业还是有一定难度的。原有基础上Group中可以删人了,这就得考虑把ageMean等值减回去的问题。虽然说可以删人,但是删人的时候本来就需要遍历来更新一些数据,因此继承第二次作业的ArrayList+HashMap结构问题也不大。不过本次作业最大的难点是这些:
queryStrongLinked:我采用的是两次bfs的方法。起先我采用了两次dfs的方法,但用Junit测试时发现数据量比较大时dfs陷入了死循环(也可能只是执行时间太长),因此改用了bfs。这两种方法的基本思想都是参数中传入一个visit容器来作为已经访问的节点以便第二次访问时忽略这些点。但无论哪种方法都存在一个致命的问题:第一次找到的某路径上的点可能会卡掉第二次的路径,但是其他路径则不会卡掉。解决这个问题的方法也很简单,就是让它找遍所有路径才会停下来。虽然此方法复杂度极高,但考虑到指令数量的限制,该方法并不会导致超时。
queryMinPath:这个虽然是难点但其实没啥好说的。我开始时采用了朴素的迪杰斯特拉算法,算法中visit等容器都使用了HashMap来提高性能。但是事实证明朴素的迪杰斯特拉算法是不行的,因此强测有3个点TLE。后来我采用了堆优化的迪杰斯特拉算法来修复bug,成功地通过了这些点(不过写到这里的时候该bug修复还在审核中)。
queryBlockSum:前两次作业的isCircle我都是用的bfs,但这次看到这个函数则不得不改用并查集。具体做法则是用HashMap来给映射编号与块(容器),加人时分配一个新的容器并记录其所属块编号,link时就可以通过双方的所属块编号快速找到相应容器并进行合并操作(这里我是让编号大的块向小的看齐),同时更新相应容器中成员的所属块。由于采用了并查集,isCircle性能大大提升,因此在进行queryMinPath和queryStrongLinked操作时就可以先判断他们是否连通以避免做无用功。
本次作业虽然强测被卡了3个点,但是互测则没法卡。但由于我想皮一下手抖把迪杰斯特拉算法中的无穷大值设置成了114514,导致被一个路径长度超过114514的数据HACK。然后强测中发现别人的错误也是五花八门,有和我一样无穷设置过小的,有加关系时id相同抛出异常错误的,有最短路径直接找错的,有借钱借错的……我都很好奇他们怎么过的中测……
三、心得体会
写规格一点也不比写代码容易。但是作为一个“三方协议”,规格还是能够发挥很大的作用。在本次作业中最直观的体现就是按照规格写起来比前两个单元的作业写起来要轻松得多(不算数据结构部分的话)。虽然写规格很麻烦,但是比较好的是第三单元重在对jml的理解。只有全面理解了规格,才能确保自己实现时的正确性。不过按照规格并不意味着我们应该放弃架构。我们应该设法在满足规格的前提下尽量提升性能。