OO第三单元总结
1. jml基础概念
JML,即java Modeling Language,是用于对Java程序进行规格化设计的一种表示语言。其利用前置条件、后置条件、不变式等约束语法,描述了Java程序的数据、方法和类的规格,是一种契约式程序设计的实现工具。在本单元中,jml仅使用了部分level 0语法,所以在这里也只总结本单元出现过的语法。
1. 原子表达式
\result: 方法(非void类型)执行后的返回值。如"\result == 0"代表需要返回0。
\old(expr): 表达一个表达式expr相应方法执行前的取值。如"old(people.length) = people.length - 1"表示people数组长度加1。注意old(people.length)与old(people).length有所区别,如果不改变地址的话old(people)与people表示的都是同一个变量。
2. 量化表达式
\forall: 全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。"\forall int i; f(i); g(i)"表示对任意的i,如果满足f(i)则必须满足g(i)。
\exists: 存在量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。"\exists int i; f(i); g(i)"表示存在i,满足f(i)且满足g(i)。
\sum: 返回给定范围内的表达式的和。"\sum int i; f(i); x"表示对i,如果满足f(i)则在sum中加x,这个x可以是常量也可以与i有关的变量。
3.操作符
<==>, <=!=>: 等价关系操作符,对应等价与不等价。
== >,< ==: 推理操作符,对应充分条件和必要条件。
\nothing, \everything: 变量引用操作符。对应不包含任何变量和所有变量。
4.方法规格
requires: 前置条件
ensures: 后置条件
assignable, modifiable, pure: 副作用范围限定。assignable表示可以被赋值的量,modifiable表示可以被修改的量(比如变量的地址改变)。pure表示没有副作用,可以在其他方法的jml中调用pure的函数。
signals, signals_only: 异常抛出语句。signals需要在后面判断额外条件,signals_only仅需满足require。
2. jml工具链
jml常用的工具链有SMT solver、jmlunitNG等。
SMT solver装不上。
jmlunitNG对MyGroup进行检查,结果如下:
emm我不太知道这种到处爆找不到符号的检查能有啥用。
3. 架构整理
这次都是在助教给定的架构下进行编写,架构都比较统一,仅在于实现上的区别,在此简述一下部分自己使用的优化方案。
1. 使用HashMap降低查找复杂度
在person类中,我最开始使用了HashMap
2. 使用缓存机制
在Group中的getValueSum与getRelationSum时使用了缓存标记valueSumValid与relationSumValid,当计算完毕后将缓存标记设为true并存储返回值,若relation关系无变化直接返回保存值,仅当relation关系变化时将缓存标记设为false。同时在Group中的queryAgeMean中进行实时计算,queryAgeVar由于有取整的坑和中途爆int的风险,采用和上面相似的缓存标记机制,只是在addRelation时不需要更新。通过缓存机制可以避免很多无用的计算。
3. 并查集
在network中使用了blocklist的一个队列,里面存储着HashMap
4. 堆优化
在queryMinPath中使用priorityqueue进行堆优化,提高查找distance最小点的效率。(最开始是用arraylist通过不断删除插入维护一个有序数组,不仅代码冗余且在删除时查找对应元素仍然需要遍历,后来才知道有priorityqueue的现成轮子)。
5. tarjan算法
在queryStrongLink中使用了tarjan算法寻找割点,通过排除割点查看连通性来判断两个点是否为点双连通分量。
4. BUG查找与修复
第九次作业,由于jml规格较为简单,直接用代码翻译一遍即可,所以没有做过多测试。在强测与互测中均未被发现BUG,互测中也未发现其他人的BUG。
第十次作业,由于frb与一些杂七杂八的事情导致分配在oo的时间急剧减少,没有进行系统的测试,未进入互测。发现BUG为queryAgeMean除零与queryRelationSum的计算错误。
第十一次作业,痛定思痛,进行了对拓展方法的junit编写,以下以quqeyMinPath的Junit函数为例。
该函数测试了普通情况下的最短路径(连通,成环与非成环,与非连通),对可能exception进行了测试,对mynetwork中的person进行了addrelation操作再进行检查,对于queryMinPath的正确性有较为全面的覆盖。
由于上一次作业遗留了很多性能问题未修复,如果直接照搬会超时,于是在很多地方都进行了调整,加上junit没有时间写全覆盖所有函数,只写了新增的几个函数,所以导致第十一次作业情况也不太理想。在强测和互测中我被发现的BUG有未进行堆优化而造成的dijstra算法超时,queryRelationSum再次书写错误(为了优化我换了一种写法,比较network中的people与oerson的acquaintance数组大小,在较小的一个数组中进行遍历寻找,在遍历acquaintance中,由于islink是自反的,我进行了自增,但是在遍历network中不需要进行自增。由于自测的时候较为匆忙,没有测试遍历network的情况导致BUG产生),非常可惜。至于互测中发现别人的BUG,由于这次不太理想预计被分到了C屋,通过自己编写的Junit就爆出了其他人的很多BUG,包括iscircle未自反,borrowmoney手滑写错,delperson出现问题,queryblocksum直球翻译规格导致超时,getAgeVar除零等等。
5. 总结与感想
这个单元整体难度和前面相比不能算大,但是对细心程度要求更高了,可以说是“千里之堤溃于蚁穴”,有不少人在第九次作业因为一个BUG直接爆0,也逼同学对程序进行全面的测试。jml对程序的要求进行了规格化的描述,而我们在编写程序时,要用离散数学的知识读懂规格,同时又不能局限于规格,要把眼光放在全局来进行设计而不是简单做一个翻译规格的机器。我认为这样使用非自然语言来进行规格叙述是有一定的意义的,避免了程序设计沟通时使用自然语言所产生的歧义,我在这个单元也学习了很多关于优化的思想和junit的使用。
但同时我还是想在最后质疑一下jml的重要性。首先jml的工具链十分老旧且难用,其他博客里大多数同学也有反映关于工具链的问题,一是太难配置难以兼容新的JDK版本以及各种未知错误,二是局限性太大,对于程序测试的覆盖其实仅仅是一些基本没有什么用的情况,对于真正有bug的地方很难测试出来。其次我对于jml应用的广泛性也有所质疑。在网络上搜索jml,几乎除了学长的博客没有其他任何迹象表明jml在实际的软件开发中有所使用。并且在这几次作业,需求尚且如此简单(相比于真正的工程设计),但是jml规格的代码量也非常大,这无论是对于需要阅读规格的程序员,还是对写规格的设计师(助教隔三岔五地修改规格,说明确实实现稍微复杂一点的要求规格编写难度就会提升很多(非对助教与老师不满,仅就事论事))都是非常难受的。有一些很容易通过自然语言进行叙述的要求(如queryBlockSum)却要以一种很抽象的方式叙述。我个人认为在自然语言和非自然语言的描述中可以做一个平衡,在一些自然语言难以叙述的时候才使用jml作为一个辅助,使其作为一个更加灵活的,类似于Javadoc的工具,而不是把规格奉为圭臬,强迫程序员做出削足适履的事情。