BUAA_OO 第三单元总结博客

OO第三单元总结

写在前面

本单元的作业是根据JML规格实现社交网络系统,比起前两个单元,本单元可以说要轻松许多(如果不看性能问题,真的很像阅读理解填空^_^),只要按照规格写,中测总是很好过的(虽然不好好测试的话强测一定会炸),总而言之,这一单元作业比起前面两个单元而言比较简单,完成较为容易。

实现规格所采取的设计策略

实现规格的方式

在第九次作业库中课程组提供了JML_level0手册中,在每次作业中,大部分的JML规格说明也十分明确地告诉了我们各个函数的实现,在实现规格的过程中,我主要采取以下步骤:

  1. 大致浏览所有接口规格,了解社交网络的大概功能和属性。

  2. 仔细阅读Person,Group,Network等接口规格,了解该接口内的函数功能并进行实现,由于该部分接口实现函数基本只与本类中的属性有关,因此可以不依赖其他类的实现,直接完成函数较为简单。

  3. 阅读指导书和Runner类,大致了解各个输入指令的功能以及输入数据。

  4. 阅读Network接口中的函数名称,将其中函数与各个输入指令对应,并通过函数名和规格了解功能。

  5. 阅读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作为容器。

性能分析

第一次作业

本次作业可能出现的问题主要在isCirlcequeryBlockSum两个函数上,在isCircle函数中,需要判断两个Person是否连通,如果采用bfsdfs等方式有可能出现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*_*)

为了解决这些问题,我主要在下面几个函数中进行了改动,提高了性能:

  • MyGroupgetAgeMean函数和getAgeVar函数

    为了降低这两个函数的时间复杂度,我使用ageAllageQart两个变量分别存储group中各个Person的年龄总和和年龄的平方总和,在实现getAgeMean函数和getAgeVar函数时只需要根据这两个变量进行计算即可,在每次addPersondelPerson后更新相应的值,即可保证两个函数结果的正确性。

  • MyNetworkqueryBlockSum函数

    由于在改动之前该函数的复杂度为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;
        }

第三次作业

在第三次作业中,我大部分架构沿用第二次,只有MyGroupMyNetWork中的部分函数进行了改动:

  • MyGroupgetValueSum函数

    在第二次作业中,该函数使用两层for循环嵌套得到相应结果,时间复杂度较高,容易出现CTLE问题,因此,在第三次作业中,我使用sumVal变量存储一个groupvalueSum的值,当addPersondelPersonaddRelation的时候更新该值,避免了双重循环所造成的高时间复杂度。

  • MyNetworksendIndirectMessage函数

    在本次作用中,为了确保程序在规定时间内运行结束,我使用优先队列,用堆优化的迪杰斯特拉算法完成了这个函数:

    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的时候耐心地帮我找问题~

大家真的都超级nice!!!

posted @ 2021-06-01 20:05  ZIMUQIN  阅读(55)  评论(0编辑  收藏  举报