Summary of OO Unit3

一、JML理论基础、应用工具链情况

JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。诞生于契约性编程思想,JML能够以javadoc注释的方式规范地描述方法和类型的规格,使之成为设计者与开发者之间共同遵守的契约。正如同商业活动中的契约一般,在设计开发过程中双方都必须遵守契约中的规定,履行自己的义务,尤其是代码实现人员要按规矩办事,在方法规格、类型规格的指导下完成代码的开发。

相比于自然语言,以JML描述规格有两大好处。一是逻辑规整严谨,代码实现人员不会在字面解读和逻辑推导的过程中感到模棱两可;二是能够交由机器进行形式化验证,进行自动的分析和推导,消除歧义,找出错误,极大程度地节约了人力和时间。

如何达到上述功能呢?这依靠着JML的三大要素:表达式、方法规格、类型规格。

表达式

语言离不开语法,表达式的内容正如同JML的语法结构,主要包括原子表达式、量化表达式、集合表达式和操作符,它们是对Java表达式的扩展,可读性强、语义清晰,具体内容可以查阅JML Level手册,这里不再赘述。

方法规格

规定了方法满足什么输入条件、产生什么执行结果,而不关心具体的实现过程。核心内容包括三个方面,前置条件、后置条件和副作用约定,顾名思义就是分别规定了对方法输入参数的限制、对方法执行结果的限制与执行过程中修改成员时的限制。同时,为适应用户的所有可能输入,方法规格往往还规定了对不符合前置条件的输入的异常处理。

类型规格

针对程序中定义的数据类型所设计的限制规则,主要涉及不变式限制(invariant)和约束限制(constraints)。前者描述了某个对象在所有可见状态下都必须满足的特性,后者则规定了某个对象的变化规则。

JML目前主要有以下应用工具链:

OpenJML:用于验证JML的工具,可检查JML语法的规范性,也可以验证Java程序是否符合规格。

JMLUnitNG:可用于自动生成数据检测程序。

二、JMLUnitNG测试

[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)
Failed: <<MyGroup@80fac899>>.addPerson(null)
Passed: <<MyGroup@156bdca>>.addRelation(-2147483648)
Passed: <<MyGroup@fea376a12>>.addRelation(-2147483648)
Passed: <<MyGroup@70a931ee38>>.addRelation(2147483647)
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)
Passed: <<MyGroup@449b2d27>>.equals(null)

===============================================
Command line suite
Total tests run: 16, Failures: 6, Skips: 0

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

Process finished with exit code 0

从测试结果可以看到,JMLUnitNG生成的数据主要为边界数据,检测覆盖面低,并不能很好的验证程序的正确性。

三、作业架构设计

十分惭愧,在三次作业中我没有主动采用一些好的设计模式为代码解耦,每次都是照搬上次的代码+增添新功能,这让程序仅有的那几个类显得很臃肿。

第一次作业

本次作业的绝大多数代码我都照搬了规格的内容,但在容器的选择上,考虑到HashMap更高效的搜索功能,而且每一个Person对应独一无二的value、id,因此我在MyNetwork中使用HashMap<Integer, Person>存储people,在MyPerson中使用HashMap<Integer, Integer>存储acquiantances。

对于MyNetwork.isCircle方法,当时为预防下一次作业需要实现delRelation的功能而使用了dfs。

第二次作业

本次作业新增了Group接口,实现一些查询组内信息的方法,我沿用了第一次作业的思路使用HashMap<Integer, Person>存储组员,其他方法依然是照搬规格的内容,包括MyGroup.getRelationSum,My.Group.getValueSum的双循环,此外为盲目追求代码简洁,对MyGroup的其他一些方法还使用了在串行迭代下效率比for低得多的Lambda表达式+stream流操作,因此直接导致了大量的ctle。

经过bug修复,除了其他方法恢复使用for循环,对MyGroup.getRelationSum,My.Group.getValueSum我更改为在加入人的时候维护变量relationSum与valueSum,即在MyNetwork.addRelation,MyGroup.addPerson进行O(n)的维护,最后在查询时直接输出变量即可。

第三次作业

本次作业有以下重点:

MyNetwork.queryBlockSum:查询连通块个数操作,我选择了基于bfs进行统计,遍历图中所有人,当队列为空说明之前经过的人处于同一块中,对其加以记录,最后统计块数即可。但是缺点也很明显:每次查询时都要计算,难以进行维护。所幸复杂度不高,并没有因此超时。使用并查集是一个更好的选择,添加人与关系时可以实现高效的维护,查询结果时可直接返回结果。

MyNetwork.queryMinPath:最短路径查找,我选择了堆优化+dijkstra,为此新增了Path类实现堆的维护,每一次都进行O(nlogn)的查询。

MyNetwork.queryStrongLinked:判断点双连通分量,可惜我处理的十分糟糕,起初使用tarjan算法判断,但是在子节点参数更新和退栈操作上没有处理好逻辑关系导致测试中qsl错误频发,后来发现枚举删点+bfs即可保证性能与正确性,很好。

四、bug情况

第一次作业:没有出现bug。

第二次作业:MyNetwork.addtoGroup异常情况判断错误、MyGroup中双重for循环、Lambda表达式+stream流操作导致超时。

第三次作业:MyNetwork.queryStrongLinked正确性问题。

他人的bug:qbs、qsl均存在正确性问题,构造了一些数据成功hack。

五、心得体会

JML让我感受到了契约性编程、规范化编程的魅力,在规格的指导下,代码的实现过程变得更高效、方向更明确,对代码的验证、勘误变得更容易,对代码逻辑、结构的把握也更为透彻。

但是,规格终究只是一个宏观的指导,遵守规格固然重要,但是开发者的思考力、创造力不能受此禁锢,规格内容≠代码内容。

posted @ 2020-05-23 10:27  ·LBJ  阅读(137)  评论(0编辑  收藏  举报