BUAA OO 第三单元总结
前言
在经历了痛苦的第一单元表达式求导作业反复重构作业,相当玄学的第二单元多线程电梯作业后,我终于来到了相对来说比较摸鱼的第三单元面向JML规格编程作业了。比起前两个单元,本单元的作业可以说是相当地小清新了,只要能将官方包里的规格描述理解清楚,并选择好适当的算法,就能顺利地通过中测了。
总结分析实现规格采取的设计策略
本单元的作业主要目标是根据规格编写程序,其中主要有两个步骤:
-
理解规格。课程组在第一次作业的官方包中附带了JML Level 0手册,这对于我们理解JML中各种关键词以及符号的用法有很大的帮助。在JML对方法的的注释中,有三种子句,分别是
requires
,assignable
和ensures
。其中requires
代表该方法的前置条件(pre-condition),也就是调用该方法需要满足的条件;assignable
是该方法的副作用,也就是该方法能够修改的类成员属性;ensure
是后置条件,也就是执行该方法后的结果。在阅读规格时,需要重点理解这三个子句以及它们作用范围内的JML表达式的具体含义。除此之外,还可以根据方法的名称,参数表和返回值大胆猜测方法的具体内容,例如各种add,get类方法,光看名称就能够猜出它想要做什么,而一些规格较长的方法,也可以通过方法名称辅助理解规格,例如addRelation
方法是给两个人添加关系,addToGroup
方法是将人拉入群聊等。 -
根据规格编写方法。在编写方法时一定要细心,因为规格是需要严谨地描述一个方法的,因此免不了繁琐,特别是括号套娃的情况会很常见,如果漏看括号,就有可能会导致实现起来出了偏差。还需要注意一些附加情况,例如
addToGroup
方法中提到了当一个Group中人数多余1111人后就不能再加人了,我最开始就被这个坑到了,没有注意这个条件,导致了错误不过后来课程组又对这个点进行了重测。在实现规格时,也需要考虑方法的复杂度,尽量采用一些降低复杂度的容器和算法,以避免评测时CTLE。具体的使用将在下文中叙述。
结合课程内容,整理基于JML规格来设计测试的方法和策略
本次课程可以使用Junit单元测试来对自己的程序进行测试。JUnit单元测试框架是由Erich Gamma和Kent Beck编写的一个回归测试框架,主要用于Java语言程序的单元测试。它是一种白盒测试,也就是说测试者知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。它主要是通过断言机制来进行测试的,也就是将程序预期的结果与程序运行的最终结果进行比对,以确保正确性。在测试时它也可以给我们看到测试用例的覆盖率,能更清楚地了解测试的过程。
但实际上,Junit并不是万能的,由于测试代码需要自己编写,因此仍然可能会存在一些意料以外的bug。在研讨课上,分享Junit测试方法的同学被问到“Junit有什么用”时,他也坦言“感觉没什么用”。因此也需要自己进行静态代码分析或者编写一些测试用例来进行测试。
总结和分析容器使用的经验
在本单元的作业中,由于需要考虑性能,我在存储相关的类时主要使用Hashmap
进行存储。在Hashmap
中进行插入,删除,查找等操作时间复杂度都为O(1),效率很高。由于每个Person
,Group
和Message
都有自己独一无二的Id,因此我选择在存储时将它们的Id作为Hashmap的key,这样在搜索时也较为方便。
除此之外,在第三次作业的sendIndirectMessage
方法中,需要使用到堆优化迪杰斯特拉算法来求最短路径,这里的堆优化指的是小顶堆,我使用了java的PriorityQueue
容器,它本身就是一个已经封装好的小顶堆,使用起来也较为方便。
对本单元容易出现的性能问题进行分析
首先,在本单元的作业中,优先考虑使用Hashmap
对相关数据进行存储,因为Hashmap
在插入,删除,查找相关元素时效率较高。
在第一次作业中,比较容易出现性能问题的是isCircle
方法和queryBlockSum
方法,这两个方法分别是判断两点是否联通和计算连通分量个数的。在最开始,我只是简单地使用了广度优先搜索算法来解决这两个方法,然后在强测中不出意料地CTLE了。因此,在bug修复环节,我改为使用并查集算法来完成这两个方法,主要为新增一个HashMap<Integer, Integer> fathers
容器来表示熟人间的父子关系,并实现了一个find方法进行路径压缩以及返回父节点。find方法具体如下:
private int find(int id) {
if (id == fathers.get(id)) {
return id;
} else {
int father = find(fathers.get(id));
fathers.replace(id, father);
return father;
}
}
在第二次作业中,主要需要对输入的数据进行预处理,以防止在输入元素较多时,查询相关的信息出现CTLE的错误,例如,在计算年龄的平均数和方差时,未避免遍历时间过长导致CTLE,可以在每次新增一个元素时预先将年龄总和,年龄平方和累加,最后根据公式直接求平均即可。
在第三次作业中,sendIndirectMessage
方法需要用到迪杰斯特拉算法,容易超时,因此可以考虑使用堆优化的迪杰斯特拉算法。为了能够直接通过Person
的Id获取它与头节点的value,我新建了一个类用于存储这两个信息,并重写了compareTo
方法,用于比较两个对象之间的大小,如下所示:
public int compareTo(RelatePerson o) {
if (o.getValue() > value) {
return -1;
} else if (o.getValue() == value) {
return 0;
} else {
return 1;
}
}
java已经为我们封装好了小顶堆容器PriorityQueue
,直接使用就好了。在PriorityQueue
容器中,使用poll
方法可以直接返回堆顶元素,也就是目前距离头节点最近的元素,复杂度为O(logn) ,比朴素写法的O(n)要快一些。
梳理自己的作业架构设计,特别是图模型构建与维护策略
由于本次作业的主要架构已经通过官方包和JML建立好了,且三次作业是迭代开发的,因此以下仅列出第三次作业的架构图(为了使UML类图更为简洁,略去异常处理类和类的方法):
本单元为建立一个关系网络,但是架构并没有采用传统的邻接矩阵或者是邻接图来完成,而是使用了“分布式”的存储方法,通过一个Network
类将所有的Person
,Group
和Message
存储起来,Person
之间的关系则由每一个Person
对象单独进行存储。这样的优点是明确了各个类之间的分工和关系,维护起来更方便,更具有面向对象的特征。
在架构中,Person
类通过两个Hashmap
记录下与它有关系的点,这两个容器就相当于分别存储了图的边和对应的权重,每次添加关系的同时更新这两个容器即可,较为方便快捷。
一些感想
本单元的主题是JML,难度不大,重点在于掌握JML注释的语法,并通过JML写出正确的代码。也正是由于难度不大,因此似乎题目更偏重于考察算法的设计和优化,个人感觉像是在复习预习数据结构。不过这一单元也让我对规格有了一定的了解,明白了规格的重要性。