第三单元作业总结

第三单元作业总结

本单元不同之前两个单元,我们不再是按照自己的意愿想怎么写就怎么写,而是必须仔细阅读JML,完成JML中的“规定动作”,保证实现的代码能够到达JML的要求,这就对我们的书写代码时的细致程度、测试强度有了更高的要求。

一、JML基础及工具链

JML基础

JML(Java Modeling Language)是一种对JAVA程序进行规格化设计的语言,是一种形式化的契约规范语言。JML书写者需要保证写下的JML无二义、无矛盾,阅读JML、进行代码实现的人需要保证实现JML中规定的功能。二者均需要承担一定的义务,因此JML是一份双向的契约式语言。

量化表达式
表达式 意义
\forall 全称量词,表示范围内所有的元素均满足条件
\exists 存在量词,表示在范围内存在元素均满足条件
\sum 返回给定范围内的表达式的和
操作符
表达式 意义
<==> 等价关系,且比 == 优先级低
<=!=> 与<==>含义相反,且比 != 优先级低
==> 推理表达符
原子表达式
表达式 意义
\result 返回值
\old(expr) expr在执行操作前的值
\not_assigned(x,y,...) 括号中的变量如x和y在执行过程中没有被赋值
\type(expr) expr的类型

配套工具

OpenJML:OpenJML的作用在于可以检查JML规格的正确性,提供对程序的静态和动态检查。但是由于OpenJML无法识别exist、三目运算符等高级语法,因此在实际使用的过程中效果不好。

JMLUnitNG:用于自动生成数据,对程序进行检测。先前得知JMLUnitNG生成的数据多为边界数据,难以对程序的正确性进行检验。因此,在强测之前没有采用这种方法进行测试,而是使用了JUnit+对拍的方式。JUnit用于测试程序正确性、鲁棒性。对拍用于大规模测试,尽可能放大代码存在的问题。

JUnit:第三单元中多次使用,在实践中发现JUnit是一件法宝。本单元中,由于已经给出了方法的规格,因此方法的输入和输出,以及方法所要达到的效果都已经确定了,能够变动的部分仅仅是实现的方式,这些特性使得使用同一份JUnit代码进行多次测试成为了可能。除此之外,性能的优化是几次作业都逃不开的环节,如何保证优化后的代码依然能够正常地行使功能就成了摆在我们面前的一道难题,而JUnit正是我们解决这道难题的钥匙。

为保证程序正确性,我在第十一次作业中大量使用了JUnit。

图片名称

除了验证正确性,JUnit还为我们提供了测试程序运行时间的功能。

图片名称



二、部署SMT Solver

由于MyGroup过于复杂,故在小的Demo中运行OpenJML。在实际部署的过程中,发现OpenJML只会对JML进行形式化验证,并没有检测代码是否实现了JML要求的功能。


图片名称

如上图,wrong方法和isNegative方法没有按照JML的要求书写,但是JML并没有针对这两种方法报错,而是提示了其他方法可能溢出warning(下图)。


图片名称



三、部署JMLUnitNG

这个工具真的不大好用,部署太蛋疼了,使用的时候各种找不到主类+Exception,感觉写博客的时候的有一般时间在部署JMLUnitNG。。。。。实际测试效果也不理想,因为自动生成的数据基本都是在测边界数据如0、INT_MAX、INT_MIN、NULL一类的数据,很难有效地检验程序是否达到了JML的要求。

[TestNG] Running:
Command line suite

Failed: racEnabled()
Passed: constructor MyGroup(-2147483648)
Passed: constructor MyGroup(0)
Passed: constructor MyGroup(2147483647)
Failed: <<MyGroup@4ea578d>>.addPerson(null)
Failed: <<MyGroup@70ab78c51b>>.addPerson(null)
Passed: <<MyGroup@156bdca>>.addRelation(-2147483648)
Passed: <<MyGroup@70a931ee38>>.addRelation(2147483647)
Passed: <<MyGroup@f0e177ab3>>.addRelation(0)
Passed: <<MyGroup@f0e177ab3>>.addRelation(0)
Passed: <<MyGroup@f0e3a164b>>.addRelation(2147483647)
Failed: <<MyGroup@d9067f96>>.delPerson(null)
Failed: <<MyGroup@c7abd898b>>.delPerson(null)
Passed: <<MyGroup@5459834>>.equals(null)

===============================================
Command line suite
Total tests run: 14, Failures: 5, Skips: 0

===============================================

Process finished with exit code 0



四、构架设计

第一次作业

本次作业最终需要实现一个社交关系模拟系统,可以通过各类输入指令来进行数据的增删查改等交互。由于本次作业的功能较为简单,因此完全按照JML书写。


图片名称

第二次作业

本次作业最终需要实现一个社交关系模拟系统,可以通过各类输入指令来进行数据的增删查改等交互。由于本次作业添加了一系列query指令,有着O(n^2)的复杂度,我采用了缓存的处理方式,在每次addToGroup时,更新MyGroup中的缓存数据,将query指令的复杂度压力分摊到每次add上。


图片名称

第三次作业

本次作业最终需要实现一个社交关系模拟系统,可以通过各类输入指令来进行数据的增删查改等交互。本次作业中添加了与图有关的算法,极大地增加了算法的复杂度。以QMP需要用的dijkstra算法为例,算法本身的复杂度为O(n^2),如果不进行任何优化,很有可能被800个Person卡出2s,造成超时。

容器对性能的影响十分巨大,为了使用更加快捷的容器,我在构架上做了少量修改,例如引入了QueueSet用于存储和比较。除此之外,为了方便测试,我还加入了Junit测试代码


图片名称

为了分摊高复杂度算法的负担,我使用了并查集。这样一来,QBS和isCircle均变成了O(1)的复杂度。虽然引入并查集简化了上述方法的实现,但是会引入复杂的更新机制,很容易出现错误或是进行冗余计算。为了避免这些问题,我才用了事件驱动的更新机制,在每次addPerson和addRelation时对并查集进行更新,保证更新机制覆盖了所有可能对状态造成影响的情况。


图片名称



五、bug情况

第一次作业

我还是天真了。。。。。我居然以为每个Person的id只会为1-3000这3000个数字,并且依次递增,并因此得到了20个WA。

第二次作业

强测互测均无bug

第三次作业

由于容器初始化开的太大了,导致强测WA了一个点,互测无bug。


图片名称

此处将InitialCapacity设为2048,导致了TLE。将初始容量设这么大的本意是省去过多addRelation后acquaintance容器扩容的时间,但是没想到过大的初始容量会使得dijkstra在遍历一个Person的acquaintance时花太多时间(Map的遍历是要遍历hash表,表越大,时间越长),由此导致了一个点TLE,改为512后AC。




六、心得体会

总的来说,本单元的难度没有前两个单元难度大,因为我们基本不需要自己去思考跟怎样设计构架(其实也不敢,尤其是第一次作业因为没有完全实现JML导致全WA造成了很大的心理阴影)。经过三次作业的洗礼,让我们充分认识到了契约式编程,即你提出的要求我完全答应,其他的我不管,但你也不能“出格”。这样的设计方式是将来步入工作后经常会遇到的,因此十分值得现在专门拿出一段时间进行练习。本单元让我认识到了JUnit的强大功能,尤其是在我们需要不断优化代码,并确认正确性的时候,JUnit可以极大地减少我们的“体力劳动”,可以说是迭代开发的神器。

posted @ 2020-05-22 16:29  摄津Osaka  阅读(140)  评论(0编辑  收藏  举报