BUAA_OO 第三单元总结博客
本单元的作业是根据JML
规格实现社交网络系统,比起前两个单元,本单元可以说要轻松许多(如果不看性能问题,真的很像阅读理解填空^_^),只要按照规格写,中测总是很好过的(虽然不好好测试的话强测一定会炸),总而言之,这一单元作业比起前面两个单元而言比较简单,完成较为容易。
实现规格所采取的设计策略
实现规格的方式
在第九次作业库中课程组提供了JML_level0
手册中,在每次作业中,大部分的JML
规格说明也十分明确地告诉了我们各个函数的实现,在实现规格的过程中,我主要采取以下步骤:
-
大致浏览所有接口规格,了解社交网络的大概功能和属性。
-
仔细阅读Person,Group,Network等接口规格,了解该接口内的函数功能并进行实现,由于该部分接口实现函数基本只与本类中的属性有关,因此可以不依赖其他类的实现,直接完成函数较为简单。
-
阅读指导书和Runner类,大致了解各个输入指令的功能以及输入数据。
-
阅读Network接口中的函数名称,将其中函数与各个输入指令对应,并通过函数名和规格了解功能。
-
阅读Network接口中各个函数的规格,并根据之前实现的Person等类中的函数实现
MyNetwork
类。
在填充类中的函数时,我将每个函数分成正常,异常两个部分分别实现,举个例子:
/*@ public normal_behavior //正常行为,首先进行实现 @ requires contains(id1) && contains(id2) && getPerson(id1).isLinked(getPerson(id2)); //使用条件语句进行判断,并实现正常时的规格 @ ensures \result == getPerson(id1).queryValue(getPerson(id2)); @ also @ public exceptional_behavior //异常部分,当该异常类型存在时直接使用,如果不存在,阅读指导书补充该类异常 @ signals (PersonIdNotFoundException e) !contains(id1); //当条件满足时,抛出此类异常 @ signals (PersonIdNotFoundException e) contains(id1) && !contains(id2); @ signals (RelationNotFoundException e) contains(id1) && contains(id2) && @ !getPerson(id1).isLinked(getPerson(id2)); @*/ //pure意味着该函数不会改变任何值,即没有任何副作用,可以在其他函数中使用 public /*@pure@*/ int queryValue(int id1, int id2) throws PersonIdNotFoundException, RelationNotFoundException;
实现规格时的踩坑经历
一定要注意括号范围!!!!
举个栗子(血泪教训
/old括号内所包含的范围: \old(getMessage(id)).getPerson1().getSocialValue() == \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() 感觉是: old(person.value) = old(person.value) + old(message).value ? 快看看讨论区有没有修改规格(bushi 其实是: Old(person.value) = old(person).value + old(message).value ! ((\sum int i; 0 <= i && i < people.length; people[i].getAge()) / people.length)) == sum 难道是: sum(getAge()/people.length) ? 其实是 sum(getAge())/people.length //这个问题让我在强测的时候完全爆炸
测试的方法和策略
Junit
测试
在本次作业中,我使用了Junit
对程序进行单元测试以确定每个函数的基本正确性,举个例子,在测试isCircle
函数时,可以使用如下代码来测试该函数的基本正确性:
public class thisTest { private static MyNetwork network = new MyNetwork(); @BeforeClass public static void before() throws EqualPersonIdException { network.ap(new MyPerson(1, "a", 1)); network.ap(new MyPerson(2, "b, 2)); network.ap(new MyPerson(6, "c", 3)); } @Test(expected = PersonIdNotFoundException.class) public void isCircle() throws PersonIdNotFoundException, EqualRelationException { //对正常情况进行测试 Assert.assertTrue(network.isCircle(1, 1)); network.addRelation(1, 2, 1000000); Assert.assertTrue(network.isCircle(1, 2)); Assert.assertFalse(network.isCircle(1, 6)); network.addRelation(2, 6, 100); Assert.assertTrue(network.isCircle(1, 6)); //对异常情况进行测试 network.isCircle(100,1); } }
Junit
虽然使用很方便,但没有办法对于程序进行较为全面的测试,Junit
更适合进行对各个模块进行单元化测试。
利用python进行对拍
在第十一次作业中,我和同学们进行了对拍,并发现了单元测试中没有发现的大量bug(比起Junit
而言,和大家对拍真的十分高效!但如果在工程领域,代码可能只有一份,这种时候Junit
这样的测试工具能够发挥很大的作用!
容器的选择和使用
在这一单元的作业中,我大部分的容器选用HashMap
安排进行存储,比起数组和ArrayList
而言,使用HashMap
的查询速度很快,复杂度为O(1), 在判断一个键值对是否存在时有很大的优势,如果使用ArrayList
进行存储,复杂度会达到O(n),消耗大量时间,降低性能。
在本次作业中,由于大部分需要存储的数据都有id这一属性,因此对于HashMap
而言能够达到很好的一一对应效果,因此,在本单元作业中,我选用HashMap
作为容器。
性能分析
第一次作业
本次作业可能出现的问题主要在isCirlce
和queryBlockSum
两个函数上,在isCircle
函数中,需要判断两个Person
是否连通,如果采用bfs
和dfs
等方式有可能出现CTLE
的问题,为了避免这种问题,我采用了并查集来完成这个函数:
//在Myperson中加入两个私有成员 private int fatherId; private Person father; addRelation() { //将两个person祖先设置成同一个 while (p1.getId() != p1.getFatherId()) { p1 = p1.getFather(); } while (p2.getId() != p2.getFatherId()) { p2 = p2.getFather(); } if (p1.getFatherId() < p2.getFatherId()) { p2.setFatherId(p1.getFatherId()); p2.setFather(p1.getFather()); } else { p1.setFatherId(p2.getFatherId()); p1.setFather(p2.getFather()); } } //使用whetherCircle函数来判断是否连通,在isCirlce和queryBlockSum函数中直接使用该函数 //判断两个person是否能够联通及判断祖先是否相同 public boolean whetherCircle(int id1, int id2) { MyPerson p1 = (MyPerson) getPerson(id1); MyPerson p2 = (MyPerson) getPerson(id2); while (p1.getFatherId() != p1.getId()) { p1 = p1.getFather(); } while (p2.getFatherId() != p2.getId()) { p2 = p2.getFather(); } if (p1.getFatherId() == p2.getFatherId()) { return true; } else { return false; } }
在本次作业中,由于选取的算法比较合适,时间复杂度较低,所以没有出现性能上的问题。
第二次作业
由于在第二次作业时,我大部分设计架构仍然采用第一次的,大部分新加入的函数也直接根据JML中使用的for循环直接进行书写,因此,本次作业我的性能出现了较大的问题(大量CTLE
*_*)
为了解决这些问题,我主要在下面几个函数中进行了改动,提高了性能:
-
MyGroup
:getAgeMean
函数和getAgeVar
函数为了降低这两个函数的时间复杂度,我使用
ageAll
和ageQart
两个变量分别存储group
中各个Person
的年龄总和和年龄的平方总和,在实现getAgeMean
函数和getAgeVar
函数时只需要根据这两个变量进行计算即可,在每次addPerson
和delPerson
后更新相应的值,即可保证两个函数结果的正确性。 -
MyNetwork
:queryBlockSum
函数由于在改动之前该函数的复杂度为O(n^2),所以极大地降低了性能,造成了
CTLE
的情况,为了降低该函数的时间复杂度,我在MyNetwork
中创建了一个新变量blocksum
对于该社交网络的连通块数量进行记录,并使用change
变量记录当前社交网络连通块数量是否需要更新。当有Person
被加入NetWork
时,blocksum
的值增加一,当增加两个Person
之间的关系时,变量change
值变为一,在执行queryBlockSum
函数时,如果变量change
值为一,则使用updateBlockSum
函数来更新blocksum
的值并返回,如果变量change
的值为0,则说明blocksum
不需要进行更新,直接返回即可。public int queryBlockSum() { if (change == 1) { //代表blocksum的值已经发生改变,需要进行更新 updateBlockSum(); //用于更新blocksum的值 change = 0; //重置change } return blocksum; }
第三次作业
在第三次作业中,我大部分架构沿用第二次,只有MyGroup
和MyNetWork
中的部分函数进行了改动:
-
MyGroup
:getValueSum
函数在第二次作业中,该函数使用两层for循环嵌套得到相应结果,时间复杂度较高,容易出现
CTLE
问题,因此,在第三次作业中,我使用sumVal
变量存储一个group
中valueSum
的值,当addPerson
、delPerson
,addRelation
的时候更新该值,避免了双重循环所造成的高时间复杂度。 -
MyNetwork
:sendIndirectMessage
函数在本次作用中,为了确保程序在规定时间内运行结束,我使用优先队列,用堆优化的迪杰斯特拉算法完成了这个函数:
public int dijistra(int id1, int id2) { //采用优先队列存储 //这里的Dist是一个新建的类,用于存储id和相应的距离 PriorityQueue<Dist> dist = new PriorityQueue<>(); HashMap<Integer, Integer> remote = new HashMap<>(); HashMap<Integer, Integer> st = new HashMap<>(); dist.add(new Dist(0, id1)); remote.put(id1, 0); while (!dist.isEmpty()) { Dist d = dist.poll(); int distance = d.getDist(); int id = d.getId(); if (st.containsKey(id)) { continue; } else { st.put(id, 1); } HashMap<Integer, MyPerson> hp = ((MyPerson) getPerson(id)).getAcquaintance(); for (Map.Entry<Integer, MyPerson> entry : hp.entrySet()) { if (remote.containsKey(entry.getKey())) { if (remote.get(entry.getKey()) > remote.get(id) + getPerson(id).queryValue(entry.getValue())) { remote.put(entry.getKey(), remote.get(id) + getPerson(id).queryValue(entry.getValue())); dist.add(new Dist(remote.get(entry.getKey()), entry.getKey())); } } else { remote.put(entry.getKey(), remote.get(id) + getPerson(id).queryValue(entry.getValue())); dist.add(new Dist(remote.get(entry.getKey()), entry.getKey())); } } } return remote.get(id2); }
架构设计
在本单元作业中,整体架构基本与JML
所叙述的一致,如图所示:
在构建图模型时,将每一个Person
作为图中的节点,Person
之间的relation
作为图各个节点之间的权重,增加Person
即相当于增加节点,使用HashMap
容器进行存取,增加relation
即相当于在相应的两个Person
中增加关系和权重。在判断两个Person
是否联通时,采用并查集算法,为每一个Person
设置一个祖先,通过判断两个person
的祖先是否相同来判断两者是否联通。在Group
类中,每当加入一个Person
,使用HashMap
作为容器存储。在接受或发送信息时,通过迪杰斯特拉算法可以得到两个节点之间的最短路径。
体会与感想
在本单元中,我对于JML
语言一类的行为接口规格语言有了更加深刻的认识,JML
语言对java
程序进行了规格化的设计,有着严格的逻辑,在JML
语言的约束下,程序能够进行形式化的验证,从而更好避免逻辑上的bug。在规格化语言完善清晰的情况下,只要我们严格按照规格设计,就能够保证程序不出现逻辑上的问题,从而避免一些奇奇怪怪的bug。
总体而言,虽然这一单元的作业比较简单,但却是我强测出现问题最多的一个单元(大概是因为过了中测时候就没有好好检查的缘故*_*!)尤其是在第二单元的强测中,由于没有进行测试,出现了大量CTLE
的问题,这也提醒了我测试真的是写程序的过程中非常重要的一环qaq 希望下一单元不要再因为测试不充分而出现一些奇怪bug了!!!
致谢
在这一单元作业中,很多同学为我提供了许多帮助,在这里向他们表示诚挚的谢意~
感谢xyp同学、mjy同学和rhx同学的我debug的过程中对我的帮助,尤其是rhx同学在我完全找不到bug的时候耐心地帮我找问题~