面向对象第三单元总结

面向对象第三单元总结

写在前面

第三单元的三次作业是JML作业,实现的功能都围绕着JML中书写的要求展开,难度层层递进。在理论课上,我一度以为按照给定的JML书写相应的代码应该是件非常容易的事情,结果最后发现自己的想法属实浅薄,但是平心而论,此次作业的难度要比之前的作业简单不少,但也有许多需要想到的细节点。

本次作业的任务是要求实现一个社交系统,此次博客按照社交系统搭建的递进顺序(同时也是作业的顺序)叙述第三单元的作业总结和反思。

一、关于JML

JML(Java Modeling Language)是用于对Java程序进行规格化设计的一种表示语言。JML是一种行为接口规格语言 (Behavior Interface Specification Language,BISL),基于Larch方法构建。BISL提供了对方法和类型的规格定义手段。所谓接口即一个方法或类型外部可见的内容。JML引入了大量用于描述行为的结构,比如有模型域、量词、断言可视范围、预处理、后处理、条件继承以及正常行为(与异常行为相对)规范等等,这些结构使得JML非常强大。

  一般而言,JML有两种主要的用法:

  (1)开展规格化设计。这样交给代码实现人员的将不是可能带有内在模糊性的自然语言描述,而是逻辑严格的规格。   (2)针对已有的代码实现,书写其对应的规格,从而提高代码的可维护性。这在遗留代码的维护方面具有特别重要的意义。

JML中有许多常用的语句,具体说来,有如下几种:

(1)requires 子句定义该方法的前置条件(pre-condition),elements.length>=1,即 IntHeap 中管理着至少一个元素;

(2)副作用范围限定,assignable 列出这个方法能够修改的类成员属性,\nothing 是个关键词,表示这个方法不对任何成员属性进行修改,所以是一个 pure 方法。

(3)ensures 子句定义了后置条件,即 largest 方法的返回结果等于 elements 中存储的所有整数中的最大的那个(\max 也是一个关键词)。

在这些语句之下,有许多表达式,一般表示方式如下:

(1) \result 表达式:表示一个非 void 类型的方法执行所获得的结果,即方法执行后的返回值。

(2)\old(expr) 表达式:用来表示一个表达式 expr 在相应方法执行前的取值。

(3)\forall 表达式:全称量词修饰的表达式,表示对于给定范围内的元素,每个元素都满足相应的约束。

(4)\exists 表达式:存在量词修饰的表达式,表示对于给定范围内的元素,存在某个元素满足相应的约束。

二、JML工具链

JML有许多有用的工具,可以保证JML的正确性或者生成测试用例等等,比如:

  1. 使用OpenJML检查JML规格的正确性,提供对程序的静态和动态检查。

  2. 使用SMT Solver验证代码与规格等价性。

  3. 使用JMLUnitNG自动生成测试数据验证代码的正确性。

三、作业分析

第一次作业

第一次作业总的来说比较简单,只需要按照JML严格实现即可通过第一次作业的弱测和强测,但是也有一些坑,比如实现方式问题:在JML规格中全部都是使用数组的方式来表述,但实际实现时究竟使用什么方式去实现则非常重要。我在一开始傻傻的使用了Arraylist来实现题目中的需求,后来发现使用Hashmap有更好的效果,造成了中途易辙的尴尬。其次就是qbs的实现问题,如果按照JML上的实现方式进行实现可能会在互测中被黑超时,应该另外采取合理方式进行实现。

第一次作业的结构如下:

本次作业的设计策略比较简单,除了qbs外基本完全按照规格进行。在容器选择方面,我后来在即将截止的时候将所有的Arraylist改成了Hashmap,比如network中的people和value,二者实际上有着明显的键值对的性质,使用Hashmap存储,既方便寻找,又节约时间。关于性能问题,本次作业最容易出现的性能问题在qbs指令中,由于JML规格中给定的方法每次判断iscircle都需要进行一次图的遍历,基于iscircle实现qbs必然会占用大量的时间,因此我另外写了一个bfs专门用于qbs指令,解决了性能问题。

第二次作业

第二次作业是在第一次作业基础上的一次延续,对于已有的类进行了扩充,并新加了一些类,但总体难度不大。然而,本次作业却是惨剧的开始,由于课下测试设置的极其简单,导致在强测过程中挂掉一片,只要通过一个点就可以进入互测。。。。。。本次作业的主要坑点仍然是TLE的问题,在保证程序正确性的条件下,如何选择最好的实现方式成为了能否通过的关键。

第二次作业的架构如下:

本次作业,我仍然延续了第一次作业的实现策略,容器的选择完全采用Hashmap,对于没有明显键值对的数据,以id作为key进行存储,这样可以实现查找和读取的O(1)的复杂度,大大降低时间。

本次作业的性能问题主要出现在图的遍历(由于第一次强测未测试qbs导致大量蒙混过关)以及过多的遍历(按照代码实现年龄平均数和方差的实现导致CTLE)。针对第一项,我将bfs修改为了并查集搜索,将复杂度降低到了O(n),使得iscircle和qbs的判断更加方便,而针对年龄平均数和方差,以及社交度的计算,我采用了cache的方法。在每一次添加人和关系的时候,修改保存的年龄之和和年龄平方和,更改相关组的社交度,于是在返回以上值时实现了O(1)的复杂度,大大降低了时间占用。

第三次作业

第三次作业是对于第二次作业的进一步扩展,新加入了多个message类,让整个社交系统显得更加完备和专业了起来。

第三次作业的架构如下:

本次作业的架构和容器选择仍然延续之前的作业,除最后的sim的实现外,其它的内容均为按照JML老老实实实现就可,总体难度不大。

本次作业的性能问题主要在于最后的sim中,由于sim需要返回最短路径,因此不得不使用迪杰斯特拉算法去获得最短路径。在实现的方法上,我使用了java自带的PriorityQueue,它利用了堆的原理,可以实现内部元素的自动排序。为了构建可排序的对象,我继承了comparable接口构建了Node类,在其中保存路径信息,并根据value的大小实现了排序,将人与人之间的社交关系网转化为一个带权双向图,将每条边构造为一个Node插入到优先队列之中,从而实现了迪杰斯特拉算法,避免了本次作业的性能问题。

关于作业架构中涉及到的图模型构建和维护,非常明显,三次作业都将人与人之间的关系网作为一个图,并根据这个图求连通度,求连通分量,求最短路径。在我的作业中,并没有针对图建立专门的容器进行存储,仅仅使用作业规格中提到的内容就可以圆满的完成对于本次作业图论部分的各项操作。

四、bug分析和测试心得

第一次作业我的程序顺利的通过了弱测和强测,没有出现问题,在互测的过程中,测试的策略主要是编织巨大的社交网并大量使用qbs,利用这样的数据我在互测中找出了三位同学的bug。

第二次作业我也没有幸免。。。。尽管进入了互测,但强测成绩很不理想,出现的问题包括RE和WA两类,二者的原因相同,主要时由于测试允许的指令条数增长了许多,但我的qbs使用了bfs的方式,由于没有开足够大的数组,导致了这两个问题的发生(这一点我在参加互测时才注意到,但为时已晚)。在修复后,发现程序会导致TLE,于是果断使用cache优化了求平均数方差和query_value的过程,并且删除了bfs采用了并查集。测试的策略与第一次相同,但在第一次作业的教训下,仍然tle的同学已经很少了。

第三次作业的原因则比较尴尬,由于书写的小小笔误,导致在dce的过程中没能将messages中的应该删除的emoji删除,导致了WA。在第三次作业的互测中,由于准备考试,并未参加互测。

关于JML测试,我的JML测试仅仅限于对每个函数进行一定的自动测试,每次只需要编写一些简单的数据,就可以实现对于每个模块的简单测试。而作业迭代开发,每次测试只需要测试新加入的内容即可,总体来说,测试比较容易。

五、心得体会

首先,通过一个单元JML的练习,能明显的感受到代码在往严谨可读可交流的方向发展,模块化的思想也体现地越来越清晰。我想这是我自身能力的一大提升。

第二,一定要仔细!一定要仔细!一定要仔细!重要的问题说三遍,自己和同学出现的许多bug都是由于没有仔细看规格导致的,在按照规格实现要求的时候一定要细心,无论是现在的作业还是以后的工作中都是这样。

第三,做好优化,在选择实现方式时要思考哪种实现方式最为合适,最为节约时间和空间,并做出合理的选择,千万不要使用笨办法。JML只是限制了功能,并没有限制类内部的结构,这一点非常重要。

最后,一定要多进行评测,面向评测机的代码不可取。。。。。。(第二次作业有幸得知数位好友均以强测零分收场)做好课下的测试再进行提交非常重要,不要出现了事故再去弥补。。。。

 

 

posted @ 2021-05-27 19:42  FrankShaw  阅读(73)  评论(0)    收藏  举报