第三单元博客作业

第三单元博客作业

PART 1 JML语言的理论基础以及相关工具链

JML基础

​ JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。所谓规格化设计,就是设计者根据具体的要求进行抽象,给出一定的条件或者约束(比如对于变量的约束,方法输入输出之间的约束),然后代码的实现者则根据设计者提供的约束来完成相对应的代码。规格更像是一种在要求提出者、设计者、代码实现者之间的协议,jml则真是这种规格化的一个具体例子。

​ 除了开展规格化设计之外,jml还可以反过来,针对已有的代码书写相对应的规格,从而提高代码的可读性和可维护性。

JML注释结构

​ jml所有的注释均以javadoc的方式实现,分为行注释和块注释两种。一般来说,和我们真正的java代码相联系,对于类中变量的规格放在类的开始,对于方法规格写在对应方法的前面。

JML方法规格

​ 一般来说,JML的方法规格包括三个核心方面:前置条件、后置条件和副作用约定。

​ ·前置条件:前置条件通过require子句表示,其中requires是jml关键词,要求调用者保证require后对应的表达式在调用方法时一定为真,如果前置条件不为真,应该也对其进行处理,抛出相应的异常。在保证对应require的情况下执行的内容用normal_behavior修饰,对于异常处理,用exceptional_behavior修饰。

​ ·后置条件:后置条件通过ensure子句来表示,其中ensure是jml关键词,要求方法的实现者保证执行了该方法后,ensure后的表达式均为真。

​ ·副作用约定:副作用指在执行方法中会对对象的属性数据或者类的静态成员数据进行修改,规格中应该明确指出副作用的范围。jml使用关键词assignable或者modifiable来指明副作用的范围。如果assignable \nothing 则代表该方法不应该对规格内任何数据进行修改

​ pure方法:在规格设计中出现的,没有任何前置条件和副作用约定的方法可以用/@pure@/来修饰

JML表达式

​ JML表达式是JML的核心,就像数理逻辑中的一些量词、谓词代号一样,只有明白了jml对应表达式的意思,才能理解这个规格到底限制了什么。在jml手册中对表达式进行了完整的叙述,这里列举出一些在编写代码过程中遇到的常见的几个表达式:

\result:表示返回值,ensure \result==1 代表该方法的返回值为1

\old(exp): 表示某数据在调用该方法前的情况,通常用来表示方法对数据的修改。ensure \old(a) == a+1 代表调用该方法后,a的数值要比原来少1;

\exist \forall:存在量词和全称量词,通常用于三段表达式中。基本上是(\exist/\forall a; exp1; exp2)表示存在a/对于所有的a,表达式(exp1->exp2)成立,与离散的中的类似。

\sum 求和符号。基本用法(\sum int i, exp1, value),表示对于一定范围内exp1的值为真时,value值的总和

JML类型规格

​ JML类型规格是对类或者接口所设计的约束规则,分为不变式invariant和状态变化约束constraint。不变式所限定的表达式要求在类的所有可见状态下均满足,状态变化约束要求对象在状态变化时需要满足一定的约束。

JML工具链

​ jml拥有完整的工具链结构,常用的工具包括openjml、SMT solver、JMLUnit等,但是感觉这些工具用起来问题很多,网上没有对应翔实的教程,总体来说很难上手。

PART 2 相关工具测试

​ 关于SMT Solver和JMLUnit,由于测试下各种报错不断,无法正常运行,最终选择放弃。而且通过和同学的讨论得知,这两种工具都有很大的局限性,最后获得的效果其实一般,实用性不高,最后也没有花时间在相应的环境配置上。

​ 在实际的相关测试中使用的是对应的Junit测试,根据jml语言使用想用的Junit测试能够做到更好地覆盖各方代码并验证正确性,因此感觉使用更为成熟的Junit进行测试会更好

PART 3 个人设计架构和BUG分析

​ 由于我本次三周作业内只进入了一次互测,进的那一次也是惨不忍睹,没刀出几个bug,于是代码分析这一块着重对自身代码的问题进行分析。

​ 由于规格设定的问题,本次作业大部分的UML图和方法复杂度分析都相似,于是在此也没必要给出每次作业的UML图和复杂度分析了。

第一次作业

​ 第一次作业的难度较小,所有的代码内容基本照着规格都可以写出来,唯一有点难度的就是isCircle方法。我一开始利用dfs算法来实现isCircle方法,但是不知道哪一步写崩了,自己测试的时候没有发现问题,交上去的时候成片的tle,后来修改无果,最终改用并查集的方法来实现。说是并查集,其实是一种假的并查集,并没有实现并查集那样的查找、合并功能,而是在每次新加person和relation的时候就执行合并操作,用数组来表示关系。虽然不正统,但最后的效率还挺高。

第二次作业

​ 第二次作业新增了group类和对应的方法,这次作业比较大的一个问题就是没有注意到get方法和contain方法的高度重复,导致每次执行了两遍,以及在queryNameRank中反复进行了contain和get操作,导致运行时间爆炸,这是一个重大bug。以及在queryValueSum和queryRelationSum这两个函数中直接套用了JML给出的双循环格式,效率较低,后来也进行了修改。最后一个bug是没考虑除以0的情况导致的re,这种纯粹是测试的失误,应当避免。

​ 题外话,第二次作业被查重查出重复率大于70%,说明我第二次作业几乎都是按照jml规格上一模一样来写的,导致我的代码最后bug频繁,优化效率不够。jml只是一种规格语言,和真正的代码实现无关,虽然直接照着jml写会有很多方便,但是会带来很多性能上的问题,今后一定要注意。

第三次作业

​ 第三次作业新增的六个方法,感觉比起面向对象JML规格设计的作业,更像是算法作业,在此稍微针对其中的几个难点函数做分析:

queryMinPath:询问最短路径。没什么好说的,最短路径的算法学过的也就两种,考虑到稳定性和性能等因素选择了Dijkstra,并且在上网查询后采取了堆优化的方法提升性能。此外,利用前面的假并查集可以轻松的获得两个点之间是否相连的判断,以及与两个点相连的所有点的集合,为之后的设定路径、开数组等提供了便利。

queryStrongLink:基本思路是先寻找一条路径,然后将找到的路径删除,再寻找一次,如果找到就是true。搜索路径的方法采用是bfs,毕竟之前dfs都写崩了,再出bug就难受了。在第一次寻找的过程中通过一个HashMap记录一个节点和父节点,得到最后的路径,并在第二次bfs的时候传入,屏蔽相同的路径。此外,在讨论区中提到tarjan算法也可以来解决这个问题,写完后发现全是bug,遂放弃。

queryBlockSum:查询连通块的个数,这个在并查集下就很简单了。如果之前isCircle用的bfs或者dfs,在这里可能需要做出修改,否则时间复杂度爆炸。

​ 总的来讲,第三次作业更像是一次算法作业,规格看起来一大堆很吓人其实很好理解,最重要的还是算法实现。但是本次作业出了三个bug,一个是由于之前没进互测遗留下来的陈年bug,一个是测试的时候想当然序号按着123456的顺序产生的bug(没有写自动化测评的痛)。最后一个tle什么都没改交上去就ac了,大概是测评机抽风了吧。

PART 4 心得体会

​ 本周oo内容学习的重点是规格化程序设计,让我们理解了什么是规格语言,并且学习了如何根据规格写出相应的代码。规格看上去更像是代码和注释之间的那一种东西,比注释更加严格难懂,但是比代码更加宽松。

​ 本周最大的领悟就是:根据规格写代码看起来很简单,实际上却没那么简单。要完美覆盖规格中的没一点、每一个符号实际上是相当不容易的,一个不留神就容易出bug。与此同时,过于依赖规格许写代码会导致写出来的代码千篇一律且效率低下,而且由于看上去和规格相似,自己就会以为代码其实没有什么问题。一开始我也因为过于按照规格来写代码,结果被判了个“疑似抄袭”,而且“抄袭”的那一次作业还没进互测,真的是双重打击。

​ 虽然这周作业看起来很简单,但是极低的得分让我意识到了自己在细节方面有很多需要改进的地方,今后应该继续努力。

posted @ 2020-05-21 13:23  yokies  阅读(142)  评论(0编辑  收藏  举报