OO2022第三单元个人总结

OO2022第三单元个人总结

本单元作业的目标是是实现一个社交关系模拟系统,可以通过输入指令的形式来实现一个以用户为节点的社交森林,训练目的主要为深入理解JML并实现具有一定复杂度的JML方法。

设计策略

​关于JML实现的过程,大致分为四个部分,一是根据JML判断该段代码块中可能出现的函数行为,并进行相关的框架设计,一般来讲不满足前置条件的行为都会出现在@exceptional_behavior中,所以为了避免出现错读前置条件导致的方法运行异常,个人在实现过程中优先实现可控异常的行为,之后再考虑@normal_behavior的情况:

/*@ public normal_behavior
  @ ...
  @ also
  @ public exceptional_behavior
  @ ...
  @*/
public void addGroup(Group group) throws EqualGroupIdException {
    if (groups.containsKey(group.getId())) {
        throw new MyEqualGroupIdException(group.getId());
    } else {
        groups.put(group.getId(), group);
    }
}

​当然这是大部分情况,存在少数情况下提前判断异常会导致额外的操作,这种时候就需要灵活考虑抛出异常的时机了。处理完行为的大体框架后,再对前置条件进行判断,并非不满足前置条件就要抛出异常,在部分情况下当输入不满足前置条件的约束时函数会返回特殊值或者直接进行返回,故这一步判断是必要的。

​判断完前置条件后观察@assignable注解,搞清楚方法是否会对对象造成改变,如果造成改变分别会改变那些属性,针对这些具体的属性再进行后置条件的阅读,进行相关代码的书写,以上为个人进行JML代码实现的设计策略。

测试方案

关于测试,课程组推荐使用使用JUnit开展针对模块的单元测试,但是这种测试手段仍然要基于个人对于JML规格的理解程度进行手动构筑,如果对JML的理解出现了偏颇再如何进行针对性构筑都很难测试出来代码存在的逻辑上的问题,而且这种手动构筑测试程序的方式并不比传统测试手段高校,所以综合考虑之下还是构造数据并进行对拍的测试手段更好用一些。

但由于本单元除了个别几个函数外,没有太过复杂的方法,所以我在对自己的程序进行测试时只针对了这些复杂方法和边界值数据进行了测试数据构造,如在qlc中生成树会随着边的增加进行更新,也就是说qlc在进行一次ar之后必须要重新构造生成树,所以针对这个问题考虑在ap和ar之后重复进行大量的qlc(上限qlc次数)可以针对代码中是否存在无效的生成树重建进行hack。

但尴尬的是由于个人时间不够充裕,并没有很好的对代码进行测试,同时也未进行过针对性hack,多亏有了JML规格进行约束,代码上并没有出现太多出问题的地方。

容器选择

为了把查询复杂度降到O1,我在本单元的代码构筑过程中在容器选择上基本以hashMap为主,但是哈希冲突八次后数据结构会退化成红黑树(或许针对这种情况进行极端数据构造能hack到人,不过个人感觉课程组会不限制的这么苛刻就没管。

Network类中,person属性、group属性、message属性和几个并查集的构造都使用了id为键,对象为值的HashMap

Person类中,将accquaintance属性和value属性使用了HashMap

异常类中使用 HashMap 存储每个 id 的出现次数,键为 id

在实现kruskal的过程中,由于社交模拟图可能是个非联通图,所以需要存储各个连通分量的边,为了避免不必要的排序,我新建了一个Edge类实现Comparable接口并使用TreeSet进行存储,避免了不必要的排序问题,CompareTo的实现逻辑如下:

@Override
public int compareTo(Object o) {
    assert o instanceof Edge;
    int value = this.value - ((Edge) o).getValue();
    if (value != 0) {
        return value;
    }
    if (this.equals(o)) {
        return 0;
    }
    return this.priority - ((Edge) o).getPriority();
}

Edge具有priority属性用来表征加边的先后顺序,方便出现权重相同的安全边时选择最早加入的那条边的实现。

性能问题

JML规格在构筑过程中只是描述程序正确性,但是在所有方法的具体实现过程中都必须保证最坏复杂度不能超过O(n2),所以在几个会出现循环的方法中,都有可能会由于疏忽而产生性能问题。

第一次作业

第一次作业中可能出现问题的点有两个:

一个是Network类中的isCircle方法,这个方法目的在于判断节点的联通性,刚读完JML规格的时候我第一反应就是直接DFS求解,其时间复杂度能达到\(O(e+v)\),后面看了讨论区助教giegie发的东西又去改成了秩合并路经压缩并查集,将时间复杂度进一步降到了\(O(1+logn)\),但是有一点值得注意,就是在进行并查集根节点的查找时,如果使用递归是存在爆栈风险的,所以综合考虑之下改成循环更为稳妥.
还有一个是Network类中的queryBlockSum方法,该方法用于给出连通分量的数目,如果isCircle使用了DFS进行求解,那么这个方法就还需要再进行几次遍历来查询连通分量,这样无疑是有损性能的,在实现了并查集之后可以在Network中维护一个变量来表征连通分量的数目,每执行一次addPerson变量就自增一,每执行一次addRelation根据并查集秩合并的结果来判断变量是否需要减一,寄两个节点加边的时候判断两个节点的根结点是否相同,相同则不变,不同则进行秩合并并将qbs的变量减一。

至于其他方法,由于没有涉及复杂算法和多层嵌套循环,所以只要认真点读JML规格一般都不会写错。

第二次作业

第二次作业可能出现问题的点有两个:

一个是Network类中的queryGroupValueSum方法,事实上在Group类的构造过程中,很多方法的返回值都可以通过把它存成一个私有属性的方式来让方法复杂度降到\(O(1)\),但即便使用循环也仅有一个方法可能会产生性能问题:getValueSum,如果按照规格所写对容器进行遍历,其时间复杂度会达到\(O(n^2)\),只需要多进行几次getValueSum的指令代码就会CTLE,所以应该尝试把这个方法的复杂度分散到addPerson, delPerson, addToGroup三个方法中,并在Group中维护一个ValueSum属性,从而将复杂度直接降到\(O(1)\)

一个是Network类中的queryLeastConnection,该方法的目的是找到所给节点生成的最小生成树,有几个点需要注意,一个是Network本质上是一个非连通图,所以需要通过构造将每个连通分量的边存储起来,个人解决方案是构造一个根节点id为键,加权边的TreeSet为值的HashMap,和qbs指令类似,每执行一次ap就为put一个当前节点编号为键,new TreeSet为值的元素,每执行一次ar就判断一下当前边要加入哪个根节点以及是否需要进行边集合并。对于最小生成树的生成问题,我使用了并查集优化的kruskal算法,复杂度为\(O(eloge)\),由于使用TreeSet作为边集所以不用考虑排序的问题进一步降低复杂度。

但是针对qlc方法仍然有问题是需要注意的,每当图中存在加边操作,这个最小生成树都是可能会发生变化的,所以在每次qlc指令的结束点都需要将qlc方法的并查集进行重置,但是这样在没有加边时(即不需要重建最小生成树)产生了冗余的计算,所以我又维护了一个根节点id为键,表征该根节点代表的最小生成树是否需要重建的boolean为值的hashMap,和一个根节点id为键,表征该根节点代表的最小生成树大小为值的hashMap,每当有加边操作就把该边所在连通分量的根节点map置为true表征需要重建,在qlc指令开始的时候先查是否需要重建,不需要重建就直接返回第二个hashmap的值,需要重建就在计算完最小生成树后替换第二个hashmap的值,并将是否需要重建的变量置false,如此来解决冗余计算的问题。

第三次作业

第三次作业只有Network类中的sendIndirectMessage可能会出现问题,该方法的目的就是查询单元最短路径,我直接用了个PriorityQueue来存边,实现了堆优化的dijkstra算法,时间复杂度为\(O(nlogn)\),人和人之间的连接关系是存在Person类里的,所以这个方法对其他方法不产生影响,大体上也就没什么要注意的点和出错的地方。

拓展

拓展内容本质上是增加了AdvertisementMessagePurchaseMessgae两种消息,所以在Network类中,一方面是要对sendMessage方法进行拓展,另一方面也要增加查询商品销售额,查询销售路径,用户收藏商品三个方法,同时为了保证查询到的商品是已经售出的还要增加两个新的异常类AdvertisementUnsoldExceptionAdvertisementSoldException表征商品是否售出。

    /*@ public normal_behavior
      @ requires containsMessage(id) && getMessage(id).getType() == 0 &&
      @          getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()) &&
      @          getMessage(id).getPerson1() != getMessage(id).getPerson2();
      @ assignable messages, emojiHeatList;
      @ assignable unSoldAdvertisementList, soldAdvertisementList;
      @ assignable getMessage(id).getPerson1().socialValue, getMessage(id).getPerson1().money;
      @ assignable getMessage(id).getPerson2().messages, getMessage(id).getPerson2().socialValue, getMessage(id).getPerson2().money;
      @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
      @             (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
      @                 (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
      @ ensures \old(getMessage(id)).getPerson1().getSocialValue() ==
      @             \old(getMessage(id).getPerson1().getSocialValue()) + \old(getMessage(id)).getSocialValue() &&
      @         \old(getMessage(id)).getPerson2().getSocialValue() ==
      @             \old(getMessage(id).getPerson2().getSocialValue()) + \old(getMessage(id)).getSocialValue();

      @ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessage) ==>
      @             (\old(getMessage(id)).getPerson1().getMoney() ==
      @                 \old(getMessage(id).getPerson1().getMoney()) - ((RedEnvelopeMessage)\old(getMessage(id))).getMoney() &&
      @             \old(getMessage(id)).getPerson2().getMoney() ==
      @                 \old(getMessage(id).getPerson2().getMoney()) + ((RedEnvelopeMessage)\old(getMessage(id))).getMoney());

      @ ensures (!(\old(getMessage(id)) instanceof RedEnvelopeMessage)) ==> (\not_assigned(people[*].money));

      @ ensures (\old(getMessage(id)) instanceof EmojiMessage) ==>
      @             (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessage)\old(getMessage(id))).getEmojiId();
      @                 emojiHeatList[i] == \old(emojiHeatList[i]) + 1);

      @ ensures (!(\old(getMessage(id)) instanceof EmojiMessage)) ==> \not_assigned(emojiHeatList);

      @ ensures (\old(getMessage(id)) instanceof AdvertisementMessage) ==>
      @              (\exists int i; 0 <= i && i < unsoldAdvertisementList.length && unsoldAdvertisementList[i] == ((AdvertisementMessage)\old(getMessage(id))).getAdvertisementId();
      @                 unsoldAdvertisementList[i] == \old(unsoldAdvertisementList[i]) + 1);

      @ ensures (!(\old(getMessage(id)) instanceof AdvertisementMessage) && !(\old(getMessage(id)) instanceof PurchaseMessage))
      @            ==> \not_assigned(unsoldAdvertisementList);

      @ ensures (\old(getMessage(id)) instanceof PurchaseMessage) ==>
      @              (\exists int i; 0 <= i && i < soldAdvertisementList.length && soldAdvertisementList[i] == ((PurchaseMessage)\old(getMessage(id))).getTargetId();
      @                 soldAdvertisementList[i] == \old(soldAdvertisementList[i]) + 1) &&
      @              !(\exists int i; 0 <= i && i < soldAdvertisementList.length && soldAdvertisementList[i] == ((PurchaseMessage)\old(getMessage(id))).getTargetId();
      @                 soldAdvertisementList[i] == unsoldAdvertisementList[i]);

      @ ensures (!(\old(getMessage(id)) instanceof PurchaseMessage)) ==> \not_assigned(soldAdvertisementList) && \not_assigned(unsoldAdvertisementList);

      @ ensures (\forall int i; 0 <= i && i < \old(getMessage(id).getPerson2().getMessages().size());
      @             \old(getMessage(id)).getPerson2().getMessages().get(i+1) == \old(getMessage(id).getPerson2().getMessages().get(i)));

      @ ensures \old(getMessage(id)).getPerson2().getMessages().get(0).equals(\old(getMessage(id)));

      @ ensures \old(getMessage(id)).getPerson2().getMessages().size() == \old(getMessage(id).getPerson2().getMessages().size()) + 1;

      @ also
      @ public normal_behavior
      @ requires containsMessage(id) && getMessage(id).getType() == 1 &&
      @           getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1());
      @ assignable people[*].socialValue, people[*].money, messages, emojiHeatList;
      @ ensures !containsMessage(id) && messages.length == \old(messages.length) - 1 &&
      @         (\forall int i; 0 <= i && i < \old(messages.length) && \old(messages[i].getId()) != id;
      @         (\exists int j; 0 <= j && j < messages.length; messages[j].equals(\old(messages[i]))));
      @ ensures (\forall Person p; \old(getMessage(id)).getGroup().hasPerson(p); p.getSocialValue() ==
      @             \old(p.getSocialValue()) + \old(getMessage(id)).getSocialValue());
      @ ensures (\forall int i; 0 <= i && i < people.length && !\old(getMessage(id)).getGroup().hasPerson(people[i]);
      @          \old(people[i].getSocialValue()) == people[i].getSocialValue());

      @ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessage) ==>
      @          (\exists int i; i == ((RedEnvelopeMessage)\old(getMessage(id))).getMoney() / \old(getMessage(id)).getGroup().getSize();
      @             \old(getMessage(id)).getPerson1().getMoney() ==
      @                 \old(getMessage(id).getPerson1().getMoney()) - i*(\old(getMessage(id)).getGroup().getSize() - 1) &&
      @              (\forall Person p; \old(getMessage(id)).getGroup().hasPerson(p) && p != \old(getMessage(id)).getPerson1();
      @                 p.getMoney() == \old(p.getMoney()) + i));

      @ ensures (\old(getMessage(id)) instanceof RedEnvelopeMessage) ==>
      @          (\forall int i; 0 <= i && i < people.length && !\old(getMessage(id)).getGroup().hasPerson(people[i]);
      @           \old(people[i].getMoney()) == people[i].getMoney());

      @ ensures (!(\old(getMessage(id)) instanceof RedEnvelopeMessage)) ==> (\not_assigned(people[*].money));

      @ ensures (\old(getMessage(id)) instanceof EmojiMessage) ==>
      @         (\exists int i; 0 <= i && i < emojiIdList.length && emojiIdList[i] == ((EmojiMessage)\old(getMessage(id))).getEmojiId();
      @          emojiHeatList[i] == \old(emojiHeatList[i]) + 1);
      @ ensures (!(\old(getMessage(id)) instanceof EmojiMessage)) ==> \not_assigned(emojiHeatList);

      @ ensures (\old(getMessage(id)) instanceof AdvertisementMessage) ==>
      @         (\exists int i; 0 <= i && i < unsoldAdvertisementList.length && unsoldAdvertisementList[i] == ((AdvertisementMessage)\old(getMessage(id))).getAdvertisementId();
      @          unsoldAdvertisementList[i] == \old(unsoldAdvertisementList[i]) + 1);
      @ ensures (!(\old(getMessage(id)) instanceof AdvertisementMessage) && !(\old(getMessage(id)) instanceof PurchaseMessage))
      @            ==> \not_assigned(unsoldAdvertisementList);

      @ ensures (\old(getMessage(id)) instanceof PurchaseMessage) ==>
      @              (\exists int i; 0 <= i && i < soldAdvertisementList.length && soldAdvertisementList[i] == ((PurchaseMessage)\old(getMessage(id))).getTargetId();
      @                 soldAdvertisementList[i] == \old(soldAdvertisementList[i]) + 1) &&
      @              !(\exists int i; 0 <= i && i < soldAdvertisementList.length && soldAdvertisementList[i] == ((PurchaseMessage)\old(getMessage(id))).getTargetId();
      @                 soldAdvertisementList[i] == unsoldAdvertisementList[i]);

      @ ensures (!(\old(getMessage(id)) instanceof PurchaseMessage)) ==> \not_assigned(soldAdvertisementList) && \not_assigned(unsoldAdvertisementList);
      @ also
      @ public exceptional_behavior
      @ signals (MessageIdNotFoundException e) !containsMessage(id);
      @ signals (RelationNotFoundException e) containsMessage(id) && getMessage(id).getType() == 0 &&
      @          !(getMessage(id).getPerson1().isLinked(getMessage(id).getPerson2()));
      @ signals (PersonIdNotFoundException e) containsMessage(id) && getMessage(id).getType() == 1 &&
      @          !(getMessage(id).getGroup().hasPerson(getMessage(id).getPerson1()));
      @*/
    public void sendMessage(int id) throws
            RelationNotFoundException, MessageIdNotFoundException, PersonIdNotFoundException;

    /*@ public normal_behavior
      @ requires contains(id1) && containsMessage(id2);
      @ assignable getPerson(id1);
      @ ensures (\forall int i; \old(getPerson(id1).hasStarAdvertisement(i));
      @          getPerson(id1).hasStarAdvertisement(i));
      @ ensures getPerson(id1).getStarList().size == \old(getPerson(id1).getStarList().size) + 1;
      @ also
      @ public exceptional_behavior
      @ signals (PersonIdNotFoundException e) !contains(id1);
      @ signals (MessageIdException e) !containsMessage(id2);
      @ signals (AdvertisementSoldException e) ((AdvertisementMessage)getMessage(id2)).isSold == true;
      @*/
    public void starAdvertisementMessage(int id1, int id2) throws PersonIdNotFoundException, MessageIdNotFoundException, AdvertisementSoldException;

    /*@ public normal_behavior
      @ requires containsMessage(id) && unSoldAdvertisementList.contains(id);
      @ assignable \nothing;
      @ ensures \result == unSoldAdvertisementList.getKey(id).getSales();
      @ also
      @ public exceptional_behavior
      @ signals (MessageIdException e) !containsMessage(id2);
      @ signals (AdvertisementUnsoldException e) unSoldAdvertisementList.contains(id);
      @*/
    public int queryAdvertisementSales(int id) throws MessageIdNotFoundException, AdvertisementUnsoldException;

    public List<Person> queryAdvertisementSalesPath(int id) throws MessageIdNotFoundException, AdvertisementUnsoldException;

心得体会

不得不说有了JML之后代码准确性提高了不少,基本上只要没读错规格就不会产生什么设计上的bug(算法写错了就确实没什么办法了),三次作业基本就是从头暴力到尾,非常之流畅,体验比之前两个单元好了不少,至于如何设计出现性能问题,至少本单元的作业来看就是把所有方法都压缩到\(O(n^2)\)大概就可以了。

posted @ 2022-06-02 09:32  Horatio201  阅读(27)  评论(0编辑  收藏  举报