BUAA_OO第三单元总结(社交网络)
概述
本单元主要学习了JML规格的书写以及理解,通过提供的JML规格实现社交网络的一系列指令。其实就是相当于,把PDF的要求文档,改编成了更加形式化的,与代码结合的JML规格。
实现规格所采取的设计策略
相比之前以PDF文档的形式提供作业要求,JML给人最大的感受就是,它将要求融合在了代码之中,其实隐去了类与类之间,函数与函数之间的关系。而不像自然语言的文档,作者为了介绍清楚,往往需要叙述上述提及的关系。
这样看来,JML相比自然语言,提供了更多的细节,但是隐去了更加高层次的关系(继承关系,调用关系...)
经过这几次作业的书写经验,为了能够理清代码结构,最重要的就是首先通读一遍JML规格,同时在实现的时候,采用自底向上的策略,也就是从最原子的类开始,逐渐向上构造,能够帮助选择更好的数据结构,更高效地维护数据
例如首先构造Person类,可以在实现这一类地过程中了解到,该类型的属性,以及可以通过哪些方法来取得该类型元素,所以,自然而然地,在NetWork类中,你知道Person有通过id取得对象的方法,所以可以建立Hashmap建立id到Person对象的映射。
在最初构造的过程中,误以为必须严格遵循JML所提供的数组,如下:
public instance model non_null Person[] people;
再后来才了解到,JML的这种书写方式只是说,这是一个容器,而不代表具体的实现。
基于JML规格来设计测试的方法和策略
- 最重要的一点,还是仔细阅读JML规格,避免理解偏差导致的低级错误
- 其次,对于复杂函数,可以通过JUnit来进行测试,JUnit便于定位错误的位置,能够很快地确保函数地初步正确
此处,我们以queryBlockSum()
函数为例,进行Junit测试:
@Test
void queryBlockSum() {
System.out.println("=========================");
System.out.println("queryBlockSum Test Begin");
System.out.println("=========================");
Network network = new MyNetwork();
Person a = new MyPerson(1,"a",1);
Assert.assertEquals(network.queryBlockSum(), 1);
Person b = new MyPerson(2,"b",1);
Assert.assertEquals(network.queryBlockSum(), 2);
Person c = new MyPerson(3,"c",1);
Assert.assertEquals(network.queryBlockSum(), 3);
Person d = new MyPerson(4,"d",1);
Assert.assertEquals(network.queryBlockSum(), 4);
Person e = new MyPerson(5,"e",1);
Assert.assertEquals(network.queryBlockSum(), 5);
network.addRelation(1,2,1);
Assert.assertEquals(network.queryBlockSum(), 4);
network.addRelation(3,4,1);
Assert.assertEquals(network.queryBlockSum(), 3);
network.addRelation(1,3,1);
Assert.assertEquals(network.queryBlockSum(), 2);
}
- 利用测试程序,生成随机数据进行对拍
本单元,主要利用了python的networkx库中的函数,并利用python生成随机数据,调用java子进程进行了测试,可以很好地测试程序的整体功能,但是对于CPU_TLE的错误测试,有所欠缺
- 构造极端数据进行测试
第一次作业中,被通过加入一面包车人(addPerson),然后调用queryBlickSum()导致的CPU_TLE而hack
在以后的测试中,可以通过对于复杂度较高的函数,构造大量的调用来测试是否会出现CPU超时的问题
容器选择和使用的经验
在本单元的作业中,JML规格仅仅给出了容器需要达成的功能,而没有指定具体需要使用的容器,单纯地跟随规格,傻傻地使用静态数组,显然是会爆炸(各种意义上),为了达成良好的性能和系统的可维护性,需要选择合适的容器存储数据。
- 对于关联度高的数据(例如
person.id
和person
)和需要进行同步化处理的数据(例如emojiid
和emojiheat
)非常建议使用HashMap- HashMap可以达到更高的O(1)的查找效率,对于指令所需的大量查找操作,显然是非常好的
- HashMap便于数据的维护,对于同步化处理的数据,可以通过一次更改,同时改变两个数据的值,因此,更容易维护,避免了手残
- 临时的需要保持对应的数据,可以考虑Pair来保证数据对应
- 对于需要维护FIFO的数据(Person.messages),考虑使用LinkedList,提供更好地插入和删除效率
性能问题
总体上,采用了HashMap,加速了查找效率
另外,对于以下函数,进行了特殊处理
queryGroupValueSum(), queryGroupAgeMean(), queryGroupAgeVar()
对于这三个函数,他们的共同特点是,计算的时候,需要遍历group,queryGroupValueSum()甚至需要双循环,如果每次调用都要重新计算的话,复杂度过高,因此调整为静态查询
例如,对于函数queryGroupAgeMean(), 在group对象的内部可以维护一个ageSum的整数,存储该group中总的年龄和,该变量,每次只需要在addToGroup()和delFromGroup()函数中进行更新,相比遍历group的动态查询,效率更高,优化后的queryAgeMean()函数如下
@Override
public int getAgeMean() {
int sz = people.size();
if (sz == 0) {
return 0;
}
return (int) (ageSum / sz);
}
对于queryGroupAgeVar(),更改方差的计算方式如下
设年龄的平方和为squreSum,年龄的平均值为mean,group中的总人数为sz,group年龄和为ageSum,得到方差计算公式如下:
(squreSum - 2 * ageSum * mean + sz * mean * mean) / sz
queryBlockSum()
该函数最初通过BFS实现的,即计算遍历图所需的BFS次数,该值即为连通块的数目,但是出现了CPU的超时问题。随后改为了通过查找并查集的组数,计算连通块的数目
作业架构设计
异常类
- 首先,为每个异常类,设置了
HashMap<Integer,Integer> cntManager
类静态属性,维护每个id的异常出现次数并更新 - 设置了
int cnt
类静态变量,维护该异常出现的总次数
以MessageIdNotFoundException
为例
public class MyMessageIdNotFoundExc extends MessageIdNotFoundException {
private int id;
private static HashMap<Integer, Integer> cntManager = new HashMap<>();
private static int cnt = 1;
public MyMessageIdNotFoundExc(int id) {
this.id = id;
}
@Override
public void print() {
if (!cntManager.containsKey(id)) {
System.out.printf("minf-%d, %d-%d\n", cnt, id, 1);
cntManager.put(id, 2);
} else {
int cnt1 = cntManager.get(id);
System.out.printf("minf-%d, %d-%d\n", cnt, id, cnt1);
cntManager.put(id, cnt1 + 1);
}
cnt++;
}
}
UML类图
数据维护
主要在MyNetwork中维护一个并查集,以及通过HashMap维护各种数据
- Person
private HashMap<Integer, Integer> acq
维护联通的Person结点
- Network
private HashMap<Integer, Person> people
维护id和Person的对应关系private HashMap<Integer, Group> groups; // <id, group>
维护id和group的对应关系
以上仅举了几个例子。
一点想法
在本单元作业中,JML规格经历了非常频繁的修改,这是在以前的作业中没有出现过的,个人感觉,这也能够反映一点,JML当前的可用性依然不是很好,自己在阅读的过程中相比一般的文档,比较有困难,并没有感受到JML形式化所带来的益处。