OO第三次博客作业

 

本单元学习的内容是基于JML的规格化设计,通过三次作业实现了一个提供了基础功能的人际网络,熟悉了JML,同时也初步了解了基于规格的程序设计,还对部分数据结构和算法的知识进行了复习。

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

1. JML语言

JML是一种形式化的、面向Java的行为接口规格语言(Behavior Interface Specification Language,BISL),使用javadoc注释的方式来表示规格。

使用以JML等工具来进行规格化设计的好处有以下几点:

  1. 获得回答方法正确性问题的技术手段,只要满足规格的要求,方法就可以被认为是正确的。
  2. 规格可以作为开展测试设计的依据,通过合理的层次化设计并依据规格进行测试可以提高软件的鲁棒性。
  3. 辅助设计人员准确理解一个方法的行为。通过规格的描述可以忽略代码实现的细节。

2. JML语言基础语法

以下内容参考http://www.eecs.ucf.edu/~leavens/JML//prelimdesign/prelimdesign.html,这个网站介绍了JML语言的基础知识,有些内容在指导书中没有提到,我从中挑选了个人认为比较实用的一部分。

(1)表达式

JML的表达式与Java表达式有较大的差异,JML无法使用Java中的赋值语句、自增语句等,也无法调用一个不为pure的方法。

  1. \forall和\exists表示全称和特称量词。

  2. \max、 \min、\product和\sum分别表示最大值、最小值、连乘结果和连加结果。

    \num_of返回指定变量中满足相应条件的取值个数。

  3. \result:表示一个非void类型的方法执行所获得的结果。

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

  4. \nonnullelements:声明容器中存储的变量没有null。

    \fresh:说明一个对象/变量是新分配的,常在构造器中使用。

  5. 形如A[E1 .. E2]表示数组A中的E1元素到E2元素,A[*]可以代替A[0 .. A.length-1]。

  6. ==> 、<==、<==> 和 <=!=>用于表示推导关系,含义与离散数学中的含义类似。

(2)方法规格

  1. requires:声明方法的前置条件。

    ensures:说明方法的后置条件。

    assignable:进行副作用范围限定,常搭配\nothing和\everthing,与之相反的是\not_assigned。

  2. represents:声明一个变量对另一个变量的等价代换,例如:

    /*@ public instance represents size <- array.length; @*/

     

  3. maps-into:表示变量对应了jml规格中的model(具体实现对应抽象表示),例如:

     protected ArrayList elems;
     /*@ maps elems.theList \into theStack; @*/
     // 表示elems对应了JML规格中抽象表示的theStack 

     

  4. implies_that:给出方法规格的进一步描述。

    for_example:给出一个实际样例说明该规格。

  5. normal_behavior:表示正常功能行为。

    exceptional_behavior:表示异常行为。

    signals:表示抛出的异常和抛出条件。

(3)类型规格

  1. model:规格变量声明,可以是static类型或instance类型,分别对应静态和非静态的成员变量。

  2. initially:规定对象初始化时的限制条件,例如:

     /*@ public initially theStack != null && theStack.isEmpty(); @*/

     

  3. spec_public、spec_protected:将私有变量设置为可见。

  4. invariant:不变式,要求在所有可见状态下都必须满足的特性。

    constraint:状态变化约束,表示对象的状态在变化时满足的一些约束(与invariant类似,使用较少)。

 

3. 应用工具链

JML有相关的应用工具链,可以做到自动识别和分析处理JML规格,但是这些工具链大多都已经停止更新和维护,使用起来有很多问题。

  1. openJML:对JML注释进行语法检查。
  2. SMT Solver:与openJML配合使用,可以静态检查代码是否符合JML规格。我尝试了很久想要部署SMT Solver但没有成功。
  3. JMLUnitNG和JMLUnit:通过JML自动生成测试样例并对程序进行自动测试的工具,可以对程序是否符合JML规格进行检查。

 

4. 使用JMLUnitNG进行测试

可能是由于年久失修,JMLUnitNG的配置非常麻烦而且极易出错。折腾了很久,当它终于跑起来的时候激动得差点哭出来,但是看到它生成的测试用例又有点失望。

 OO第三次博客作业_第1张图片

 

OO第三次博客作业_第2张图片

测试的都是一些极端的情况,例如最大最小的int以及null对象。也许在一些情况下这样的极端情况测试是有必要的,但是对于这次作业来说使用这个工具用处不大。个人认为单元测试更需要使用像Junit这样的工具来构造有针对性的数据进行测试或者进行覆盖性测试,在对边界条件比较敏感的情况下才需要特地构造极端数据进行测试。

 

二、作业分析

1. 程序分析

(1)第九次作业

第九次作业较为容易,在仔细阅读jml规格的情况下不容易出错,在运行时间上也十分充裕。

在比较复杂的iscircle方法中,我使用了并查集算法,在每次addPerson和addRelation的时候维护一个link数组,在查找时只需要判断两个节点是否处于同一个集合中即可。

(2)第十次作业

第十次作业增加了Group和对Group的一些查找操作。

由于Group中人数较多,查找信息的时候如果每次都要遍历所有Person会导致超时,因此需要将这些信息以变量的形式存储在每个Group之中,使用时返回对应的变量即可。在Group中我设置了如下的变量:relationSum、valueSum、conflictSum、ageSum和ageSquareSum,并在每次对Group进行addPerson和addRelation操作的时候维护相应的变量。其中对于relationSum的维护比较复杂,需要考虑先进组再建立连接和先连接再进组两种情况,但是必须使用这样的方法防止出现O(n*n)的双重遍历查找操作。查找平均数和方差的操作复杂度是O(n),在这次作业的情况下不会出现超时,但是我还是希望尽量缩短时间,因此在Person进组的时候维护年龄之和、年龄平方之和两个变量,在计算方差的时候需要考虑无法整除带来的精度问题,必须严格符合JML规格进行书写。

在研讨课同学的提醒下,我在容器的初始化上也进行了一定的优化。

(3)第十一次作业

第十一次作业涉及的算法知识比较多,虽然数据量减小了但是更加容易出现TLE,需要选择合适的算法,并且在书写各个方法要更加谨慎,尽量降低复杂度。

在查询Group内的信息时我延用了第十次作业的方法,在新增和删去Person的时候维护相应的变量。在维护relationSum和valueSum的时候需要遍历Group中的Person,需要特别注意插入删除操作和遍历操作的先后顺序。

查询最短路径时我使用了堆优化的Dijkstra算法,时间复杂度为O(n*log(V))。由于我之前书写iscircle方法时使用的是并查集算法,在计算blockSum的时候比较容易,只需要遍历Network中的人,计算出集合的总数即可。

查询强连通的方法比较复杂,我使用了枚举每个点进行删除尝试,之后利用BFS判断id1和id2是否相连的方法。这种方法的原理是图论中的Menger定理:设x和y为图G中两个不相邻的顶点,则G中内部不相交的(x,y)路的最大数目=G中最小的xy顶点分隔集的顶点数。

若不相交的(x,y)路的最大数目为2,即x与y强连通,那么最小的xy顶点分隔集的顶点数为2,也就是说只有在删去两个点的情况下才能破坏x和y的连通性。因此,在只删去任意一个点的情况下,两个强连通的点仍然能够保持连通状态。同时,对于两点直接相连的情况还需要进行特判,临时删去两点间的关系,再使用BFS判断连通性即可。

在使用这种方法之前,我还使用了DFS的方法,就是通过搜索找出一个头尾皆是id1的圈,之后判断这个圈是否包含id2,若包含即可返回true,若不包含就继续搜索。但是这种方法的时间复杂度是指数级的,故放弃,只用作对拍使用。

 

2. 架构分析

 OO第三次博客作业_第3张图片

这个单元的架构设计比较失败,只是设计了三个类分别继承自官方接口。在听过这单元的总结课之后,我发现自己对这单元的训练任务的理解有一些误差。我本以为在架构上只需要按照接口来书写即可,应该把重点放在阅读规格并保证实现的正确性和性能。因此我除了在MyNetwork中增加了一个内部类来处理最短路径之外就没有其他的额外设计了,导致MyNetwork类中许多方法非常臃肿。

 OO第三次博客作业_第4张图片

 OO第三次博客作业_第5张图片

我认为比较合理的设计是对MyNetwork类进行一定的拆分,设置一个内部类,将与图有关的数据封装在其中。对于比较复杂的方法,例如最短路径查询、强连通查询和并查集操作,也可以设计内部类来存放。使用内部类使得每个方法更加清晰,同时也不存在访问权限的问题。

 

3. bug分析

第十次作业的互测中,同屋一位同学在计算relationSum和valueSum使用了双重循环的方法,我构造了特殊数据使其程序出现TLE。

在在十一次作业的强测中我出现了两处bug。一是在计算年龄方差时,由于用于储存年龄平方之和的数据会产生溢出,因此我将这个变量类型定义为long,但在long与int进行混合运算时我没有进行强制类型转换,导致错误。修复时我在对每个int变量都进行了手动的强制类型转换。 二是两个集中测试qsl的数据点出现超时,由于超时不超过1s,我并没有改变算法,而是在原有实现上进行优化,使得判断完成后方法可以尽快返回,优化后可以在2s内通过测试点。 在互测时我通过构造数据的方法hack到了一些错误。同屋的一位同学的最短路径查询没有使用堆优化,我构造了含有大量qmp的数据进行hack。另一位同学查询强连通的时候使用了DFS,我构造出指数级别复杂度的数据进行打击。

OO第三次博客作业_第6张图片

我还搭建了一个简易的对拍器在互测阶段使用,能够完成自动生成指令和输出比对两个功能。自动生成指令时先添加一定量的人、关系和群组,之后使用python的random.choice选取指令进行输出即可。

 

三、心得与体会

第三单元的主要内容是阅读JML规格并实现将规格进行具体实现,在实现时需要考虑算法问题,同时还需要考虑架构问题,保证代码的可维护性。总体来说就是基于规格,但是又不能拘泥于规格。

由于这一单元对于程序性能的要求较高,在完成这单元作业的时候,我结合相关博客阅读了HashMap、HashSet、ArrayList和PriorityQueue的源代码,感觉收获很大,一是已经充分了解了各个操作的底层实现和时间复杂度,在使用容器的时候更加放心,同时借这个机会也学习了数据结构课中没有掌握的红黑树和堆。

这个单元的实验更加实用了,难度也增大了很多。尤其是第六次实验介绍的GC垃圾回收的知识,我也借这个机会学习了JVM虚拟机关于垃圾回收的知识。同时也学习了JUnit这个实用的工具,感觉它检查测试覆盖率的功能非常好用。

面向对象课程已经接近尾声了,感觉这个单元作业的设计很用心,尤其是JML的书写量可能比我自己实现的代码量还要大。希望下个单元可以继续努力。

你可能感兴趣的:(OO第三次博客作业)