JML的理论基础、应用工具链
理论基础:
JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言,通过固定的语法规定了java的类中的不变式,方法的前置条件,后置条件和副作用等。JML为严格的程序设计提供了一套行之有效的方法。通过JML及其支持工具,不仅可以基于规格自动构造测试用例,并整合了SMT Solver等工具以静态方式来检查代码实现对规格的满足情况。
一般而言,JML有两种主要的用法:
(1)开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。
应用工具链:
-
OpenJML:检查JML规格的语法正确性
-
JML UnitNG:根据JML自动测试实现代码的正确性
-
SMT:验证程序等价
部署JMLUnitNG/JMLUnit
这部分放弃了,,
本单元作业的架构设计
第一次作业
第一次作业整体来说比较简单,主要是在熟悉JML,大部分方法按照JML中的要求,对时间也没有太多要求,注意各个分支的异常情况完成即可。
规格中对容器等没有顺序的要求,于是主要选择哈希表来进行存储。
MyNetwork类的addRelation方法中需要对两个MyPerson对象的acquaitance进行改变,于是在MyPerson类中增加了一个addLink方法来添加好友时改变acquaintance,在调用MyPerson的addRelation方法时要进行强制转型,这样可能不符合依赖倒置原则,但是在不修改接口的情况写似乎也没有更好的方法。
isCircle方法是这次作业中比较难的一个方法,我选择了图论中的宽度优先搜索算法,在邻接表实现的图中复杂度为O(V+E)。
第二次作业
第二次作业的指令条数大大增加了,此外还增加了MyGroup类,对时间也有一定要求。
这次作业在MyGroup类中,维护了年龄和ageSum,年龄平方和squareAgeSum,异或和conflictSum等信息,可以在查询时避免遍历;这次作业的queryValueSum和queryRelationSum如果采取双层遍历的话,会是平方复杂度,在有10万条指令的情况下有极大概率超时,所以在MyGroup类中我又增加了relationSum和valueSum,并且这两个值有可能是因为新人加群而改变,还可能由于群内已有成员互相添加好友而改变。
isCircle方法在有333条的数量限制后,超时风险很小,所以没有采用并查集的方法,保留了BFS算法。
第三次作业
这次作业与上一次的主要变化是增加了借钱功能,Group可以delPerson,NetWork中也增加了几个图论相关的算法。
brrowFrom和queryMoney的实现比较容易。
delPerson方法在实现时,要注意更改MyGroup中的一些属性;此外上次的作业在addRelation时需要判断两个Person所在的共同的Group,因此Group中删除Person的时候,这个Person所在的群组中也要删除这个Group。
NetWork中的queryMinPath方法,采用dijkstra算法,在该算法中,如果采取遍历的方式来获取未标记点中距离最短的,复杂度为O(V^2+E),超时风险很大,所以采取了优先队列优化的dijkstra算法,复杂度优化为O((V+E)logV),避免了超时。
blockSum方法采取BFS算法,类似于isCircle。
queryStrongLink是比较难的一个方法,在了解课程组很善良这件事之后,我选择了暴力的方法,大致思路如下:
如果这某个人的ID不存在
抛出异常
如果这两个节点不连通
返回false
如果这两个点之间存在边
去掉这条边判断是否仍然连通,连通则返回true,否则返回假
如果这两个点之间不存在边
判断是否去掉任意一个其他节点后,仍然联通;如果去掉任意一个节点后还联通返回true,否则返回false
BUG和修复情况
在第三次作业的查询方差方法中出现了一个int溢出导致的错误。
在存储了ageSum和squareAgeSum后,我的方差计算方法是这样的
var = (ageSquareSum - 2 * mean * ageSum + people.size() * mean * mean) / people.size();
这个算法在第二次作业中没有溢出风险,在第三次作业中我发现ageSquareSum有溢出风险,于是选择了long型来存储,ageSum仍然没有风险,但是我忽略了这个公式计算中间过程的乘积项2 * mean * ageSum + people.size()
有可能溢出,导致强测中出现了错误。
心得体会
在这个单元主要学习了理解JML,根据规格的要求实现代码,以及撰写简单的JML规格。在这个单元的作业和实验中,我体会到JML可以将设计与实现分离开,设计者只负责将需求转变为规格,实现者负责将规格转变为代码,从而能提高编码的效率和质量,提高程序的可扩展性。明确的规格还有助于将程序分成小的单位进行测试(如使用JUnit),降低错误的发生概率。