面向对象第三单元总结
面向对象第三单元作业总结
本单元需要实现一个社交关系模拟系统。可以通过各类输入指令来进行数据的增删查改等交互。
1.实现规格所采取的设计策略
本单元直接给出了基于JML的规格说明,因此在编写程序的时候,只要认真阅读了前置条件,后置条件,抛出异常等内容,实现规格还是很容易的。规格分为数据规格和方法规格两种,而方法的实现需要基于数据的组织形式。因此在基于JML编写程序时,需要先通读整个JML规格,了解各种方法需要达到的目的,再回过头来选择合适的容器来存放数据。
在数据规格的实现中,基础数据类型直接按照规格来写就行,数组则应该使用Java提供的容器类。如果需要在两个数组中寻找对应项,则两个数组需要使用Map来实现映射关系以提高查找效率。如果一个数组不需要和其他数组进行映射,则使用List进行存储即可。
在方法规格的实现中,为了便于代码的复用,应该尽可能对方法进行解耦。这一点在第三次作业体现的比较明显。第三次作业需要发送间接消息,其中对于消息的后置处理与发送直接消息是由很大的重合部分的。如果再复制一遍很容易会超过500行的风格限制。因此最好在一开始能把一些功能抽离出来,在其他方法中进行调用。
第一次作业需要实现Person,Network和几个异常类。其中异常类需要统计该异常发生的总次数,我使用了静态变量进行存储。
2.基于JML规格来设计测试的方法和策略
对于本单元作业而言,测试主要有两种方法:使用Junit进行单元测试,使用生成数据进行对拍测试。
首先我在教程的推荐下学习了Junit的测试方法。我本来以为它可以智能地 根据JML来自动测试,没想到需要自己构造测试样例并设置答案。我当时很不理解这种测试方法,感觉纯粹是绕远路。后来查阅资料才明白Junit的意义。在一些大型的程序,例如淘宝中,可能有上万个方法,运行的成本要远高于单元测试的成本。并且在现代的软件开发中,开发和测试往往需要并行工作,即如下图所示:

但是由于本单元作业程序很小,运行成本很低,因此Junit也派不上多大用场。主要还是采用构造测试数据进行对拍的方法。
3.容器选择和使用的经验
3.1第一次作业
第一次接触JML,我开始时本着严格按照规格的想法,在数据的组织上完全按照规格来写,基本都用了Arraylist来存储。在写查询的时候发现了问题,例如通过id寻找对应的人是一个O(N)的遍历过程,复杂度较高,特别麻烦。因此我采用了hashmap来存储id与person的对应关系。另外有一个查询姓名排名的方法,我选择了会自动排序的TreeSet。异常类需要统计该异常发生的总次数,可以使用一个计数器类,也可以为类设置一个静态变量,我使用了静态变量进行存储。Person类中,由于acquaintance存储的是与其直接相连的Person,所以以Person为key,他们间的value为value进行存储,方便查询。
这几个数据结构的选择中,TreeSet是我犯的一个巨大的错误。我只想到了自动排序,没有意识到它也是一个集合,重名的person会覆盖。因为生成的样例强度不够,没能在课下发现这个bug,导致课上强测直接崩盘。这说明了仔细了解底层的重要性。
3.2第二次作业
第二次作业增添了Group和Message类,需要在Network中进行对它们的组织。因为涉及到通过id查找对应对象的方法,因此我使用了HashMap来存储这两类对象。
3.3第三次作业
第三次作业增添了几种特殊的信息,其中表情信息有着自己的表情id和对应的热度。首先应该对表情id与heat建立一个hashmap,这是显而易见的。但是有一个删除不常用表情的方法,需要我们从表情的id找到对应的信息。这有两种方法:为表情的id与信息建立一个hashmap,或者使用遍历来查询。考虑到前者的维护较为繁琐,我使用了后一种方法,在删除时用了removeIf。
4.性能问题和相应的解决方法
本次作业中我遇到了三次性能问题:深度优先搜索,groupValueSum和迪杰斯特拉算法。
深度优先搜索出现在isCircle方法中,该方法需要判断两个点是否连通。我一开始使用了递归调用的写法,发生了超时问题。因此我把递归调用改成了循环,使用栈来进行深度优先搜索,解决了超时问题。
LinkedList<MyPerson> stack = new LinkedList<>();
HashSet<MyPerson> visitPerson = new HashSet<>();
stack.add(this);
boolean isfind = false;
while (!stack.isEmpty()) {
MyPerson temp = stack.getLast();
if (temp.isLinked(person)) {
isfind = true;
break;
}
boolean flag = false;
for (Person person1 : temp.acquaintance.keySet()) {
if (!visitPerson.contains(person1)) {
flag = true;
stack.addLast(((MyPerson) person1));
visitPerson.add(((MyPerson) person1));
}
}
if (flag == false) {
stack.removeLast();
}
}
return isfind;
queryGroupValueSum需要计算一个组内关系的和。我一开始使用了遍历的方式。首先遍历一遍组内的人员,对于每一个人员再遍历一遍组内的所有成员,如果二者有关系则累加在sum中。这是一个O(n^2)复杂度的遍历,在强测中出现了超时。为此有两个解决方案:在每次修改关系或者组成员的时候更新valuesum,此时的查询就是一个O(1)的过程;二层遍历时遍历人员的熟人,判断他们是否在该组中,这是一个O(n*E)复杂度的过程。考虑到每次更新value可能会产生冗余并且改动量太大,因此我采取了后一种方式,最终也成功修复了超时的问题。
原始版本的迪杰斯特拉算法的复杂度时O(n^2),为所有结点找到最短路径需要一个n的循环,这其中找到最短路需要一个n的循环,更新路径长度需要一个n的循环。外层的n循环是无法更改的,因此我主要针对内层的两次遍历进行优化:
-
寻找当前最短路:每次只要找到最小值即可。因此我采用了堆优化的方式,借助Java自带的优先队列来维护数据结构。这样,查找时候的复杂度就是O(1),插入和删除的复杂度是O(logn),最终复杂度是O(nlogn)
//优先队列声明 Comparator<MyPerson> comparator = ((o1, o2) -> { return o1.getDistance() - o2.getDistance(); }); Queue<MyPerson> priorityQueue = new PriorityQueue<>(comparator); //更新最短路 priorityQueue.remove(person); priorityQueue.add((MyPerson) person); -
更新路径长度:使用一个队列来存储还未找到最短路的结点,遍历时候在这个队列中遍历,与遍历所有结点相比可以节省一半的时间。
5.作业架构设计梳理
本单元作业只要按照规格来写即可,因此这儿主要介绍一下给定规格的架构。
- Person类:社交网络中的人,拥有自己的id,姓名,年龄,熟人等等属性。
- Group类:社交网络中的组,拥有自己的id,包含的人
- Message类:社交网络中的消息,可以是单发的消息,也可以是在组里面群发的消息
- Network类:社交网络整体的模拟。这个类中需要储存所有人的信息,所有组群的信息和所有消息。这三类对象都使用Hashmap进行组织,便于通过id来迅速找到对应的对象。在图的维护中,人与人之间的关系储存在每个人自己的属性中,即使用了邻接表的形式。
6.感想体会
本单元三次作业都非常简单,但得分却远低于前两单元的作业。这主要是由于没有认真阅读JML的规范并设计边界数据做好对拍测试导致的。第三次作业痛定思痛,构造了一些有强度的数据进行对拍,才避免了在正确性上出问题。测试在开发过程中应该占据一个很大的比重。

浙公网安备 33010602011771号