BUAA_OO_2022 Unit3 总结

BUAA_OO_2022 Unit3 总结

自测过程

数据准备

由于本单元的代码完全基于JML,因此数据生成也主要参考了JML规格。在数据生成器中,我通过参数控制各个指令的出现频率和出现顺序,以尽可能覆盖规格中出现的各类情况。

在此基础上,为了更好地覆盖边界情况以及测试程序的抗压能力,数据生成器支持生成完全图、菊花图、长链图等特殊的图论模型,支持自由控制group的个数。

下面附上一些数据生成器的一些参数

 int personId = 0, groupId = 0, messageId = 0, emojiId = 0, sendId = 0, currentEmojiId = 0;
 int maxag = 25, maxap = 5000, maxqci = 333, maxqlc = 100, maxsim = 1000, maxEmojiId = 10;
 int cntag = 0, cntap = 0, cntqci = 0, cntqlc = 0, cntsim = 0;
 
 int type = 0; // 0 normal, 1 message, 2 group, 3 graph, 4 exception, 5 complete, 6 sim, 7 emoji, 8 notice, 9 super
 
 bool noGroup = false, emojiFull = false, oneGroup = false;

 

JUnit

JUnit的核心是使用assert检验JML里的后置条件和不变式是否满足。大致思路是在需要测试的方法中,先调用初始化的方法来建立测试所需要的环境,然后调用待测试的方法,并使用assert来检验该方法运行是否正确。

下图是一个范例(by qs同学)

 @Test
 public void deleteColdEmoji() throws EqualPersonIdException, PersonIdNotFoundException, EqualRelationException, EmojiIdNotFoundException, EqualMessageIdException, RelationNotFoundException, MessageIdNotFoundException, EqualEmojiIdException
 {
     MyNetwork network = new MyNetwork();
     network.addPerson(p1);
     network.addPerson(p2);
     network.addRelation(1, 2, 12);
     network.storeEmojiId(1);
     network.storeEmojiId(2);
     network.addMessage(new MyEmojiMessage(1, 1, p1, p2));
     network.addMessage(new MyEmojiMessage(2, 2, p1, p2));
     network.addMessage(new MyEmojiMessage(3, 2, p1, p2));
     network.addMessage(new MyEmojiMessage(4, 2, p1, p2));
     network.addMessage(new MyEmojiMessage(5, 2, p1, p2));
     network.addMessage(new MyEmojiMessage(6, 1, p1, p2));
     network.sendMessage(3);
     network.sendMessage(4);
     network.sendMessage(5);
     network.sendMessage(6);
     assert network.deleteColdEmoji(2) == 1 : "dce 1";
     assert network.getMessage(1) == null : "dce 2";
     assert network.getMessage(2).getId() == 2 : "dce 3";
     assert network.queryPopularity(2) == 3 : "dce 4";
     assert network.deleteColdEmoji(3) == 1 : "dce 5";
     assert network.queryPopularity(2) == 3 : "dce 6";
     assert network.deleteColdEmoji(4) == 0 : "dce 7";
     assert network.getMessage(2) == null : "dce 8";
 }

 

架构分析

图模型的构建

本单元作业背景是社交网络模型,Person可以抽象为图中的点,Relation可以抽象为图中的边,Group可以抽象为点集。除Message相关的方法基本都是围绕着图的建立、更新和查询。

图论算法

部分图的查询方法用到了初等图论算法,qbs和qci用到了并查集、qlc用到了最小生成树、sim用到了单源最短路。

最小生成树算法我采用的是Kruskal,理由是方便复用之前已经实现的并查集算法,具体代码如下

 private int kruskal(int id) {
     HashMap<Integer, Integer> tmpFather = new HashMap<>();
     int cnt = 0;
     for (Integer x : people.keySet()) {
         if (find(father, id) == find(father, x)) {
             cnt++;
             tmpFather.put(x, x);
        }
    }
     int ret = 0;
     Collections.sort(edges);
 
     --cnt;
 
     for (Edge edge : edges) {
         if (!tmpFather.containsKey(edge.getNodeX())
             || !tmpFather.containsKey(edge.getNodeY())) {
             continue;
        }
         if (find(tmpFather, edge.getNodeX()) != find(tmpFather, edge.getNodeY())) {
             ret += edge.getValue();
             merge(tmpFather, edge.getNodeX(), edge.getNodeY());
             --cnt;
             if (cnt == 0) {
                 break;
            }
        }
    }
 
     return ret;
 }

单源最短路我采用的是堆优化的Dijkstra算法,理由是由于公测的数据限制,图在极限情况下是稀疏图,边权非负,此时堆优化的Dijkstra算法效果较优。具体代码如下

 private int dijkstra(int st, int ed) {
     HashMap<Integer, Integer> dis = new HashMap<>();
     HashMap<Integer, Boolean> vis = new HashMap<>();
 
     for (Integer personId : people.keySet()) {
         dis.put(personId, 0x3f3f3f3f);
         vis.put(personId, false);
    }
 
     dis.put(st, 0);
 
     PriorityQueue<MyPair> q = new PriorityQueue<>();
 
     q.add(new MyPair(0, st));
 
     while (!q.isEmpty()) {
         int x = q.peek().getValue();
         q.poll();
         if (vis.get(x)) {
             if (x == ed) {
                 break;
            }
             continue;
        }
         vis.put(x, true);
 
         HashMap<Person, Integer> acquaintance = ((MyPerson) getPerson(x)).getAcquaintance();
 
         for (Map.Entry<Person, Integer> entry : acquaintance.entrySet()) {
             int y = entry.getKey().getId();
             int z = entry.getValue();
             if (dis.get(y) > dis.get(x) + z) {
                 dis.put(y, dis.get(x) + z);
                 q.add(new MyPair(dis.get(y), y));
            }
        }
    }
 
     return dis.get(ed);
 
 }

数据结构思想

group中的一些查询方法若严格按照JML将会达到O(n^2)的复杂度,明显会超时。因此,需要采用数据结构的思想,维护一些中间变量,以保证单次方法的均摊复杂度在O(n)以内。

容器的选择

JML并没有限制我们使用哪种容器,因此需要我们自己根据该容器在JML中的使用特性来做出选择。本单元中查询和更新的频率相当,且基本是随机存取,因此我基本都选择了HashMap来实现单次存取O(log(n))的复杂度。一个例外是维护Person收到的Message用的是ArrayList,因为对它的每次插入和查询操作都集中在链表头部。

 

性能问题和修复情况

虽然三次作业都写了数据生成器,但是三次作业在写完后课下对拍时都没被拍出bug,公测和互测也没有出bug。(感觉数据生成器白写了)

性能问题上,由于一开始就估计了时空复杂度的范围,采用了合适的算法和数据结构来维护,因此没有出现性能问题。

在互测中,测出的问题基本也都是由于时间复杂度过大导致的TLE。

 

Network扩展

题目假设出现了几种不同的Person

  • Advertiser:持续向外发送产品广告

  • Producer:产品生产商,通过Advertiser来销售产品

  • Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买(所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息)

  • Person:吃瓜群众,不发广告,不买东西,不卖东西

显然,Advertiser、Producer和Customer均应继承自Person类;Producer会增加一个属性来维护其生产的产品的id,Advertiser会增加一个属性来维护其向外发送广告的产品的id,Customer会增加一个属性来维护其偏好的产品的id。

设置消费者偏好

 /*
     @ public normal_behavior
     @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == personId) &&
     @           (\exists int i; 0 <= i && i < products.length; products[i].getId() == productId)
     @ assignable getPerson(personId).products;
     @ ensures (\forall Product i; \old(getPerson(personId).hasProduct(i));
     @         getPerson(personId).hasProduct(i));
     @ ensures getPerson(personId).hasProduct(productId);
     @ also
     @ public exceptional_behavior
     @ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Producer)
     @ signals (ProductNotFoundException e) !(\exists int i; 0 <= i && i < products.length; products[i].getId() == productId);
 */
 public void setPreference(int personId, int productId) throws PersonIdNotFoundException, ProductIdNotFoundException;

发送广告

 /*
     @ public normal_behavior
     @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Advertiser && people[i].containsAdvertisement(advertisementId));
     @ assignable people[*].messages    
     @ ensures (\forall int i; 0 <= i && i < people.length; (getPerson(id).isLinked(people[i])) ==> (people[i].messages.length == \old(people[i].messages.length) + 1 && people[i].messages[0] == \old(getPerson(id).getAdvertisement(advertisementId)) && (\forall int j; 1 <= j && j < people[i].messages.length; people[i].messages[j] == \old(people[i].messages[j - 1]))));
     @ ensures (\forall int i; 0 <= i && i < people.length; !(getPerson(id).isLinked(people[i])) ==> (people[i].messages.length == \old(people[i].messages.length && (\forall int j; 0 <= j && j < people[i].messages.length; people[i].messages[j] == \old(people[i].messages[j]))));
     @ also
     @ public exceptional_behavior
     @ signals (PersonIdNotFoundException e) (\forall int i; 0 <= i && i < people.length; people[i].getId() != id || (people[i].getId() == id && !people[i] instanceof Advertiser));
     @ signals (AdvertisementIdNotFoundException e) (\exists int i; 0 <= i && i < people.length && people[i].getId() == id && people[i] instanceof Advertiser; !people[i].containsAdvertisement(advertisementId));
 */
 public void sendAdvertisement(int id, int advertisementId) throws PersonIdNotFoundException, AdvertisementIdNotFoundException;

查询销售额

 /*
     @ public normal_behavior
     @ requires (\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Producer);
     @ requires (\exists int i; 0 <= i && i < getPerson(id).products.length; getPerson(id).products[i].getId() == productId);
     @ \results == getProduct(productId).cost * (\sum int i; 0 <= i && i < people.length && getPerson(id).isLinked(people[i]) && (\exists int j; 0 <= j && j < people[i].products.length; prople[i].products[j].equals(getPerson(id).getProduct(productId))); 1);
     @ also
     @ public exceptional_behavior
     @ signals (PersonIdNotFoundException e) !(\exists int i; 0 <= i && i < people.length; people[i].getId() == id && people[i] instanceof Producer)
     @ public exceptional_behavior
     @ signals (ProductNotFoundException e) !(\exists int i; 0 <= i && i < getPerson(id).products.length; getPerson(id).products[i].getId() == productId);
 */
 public /*@ pure @*/ int querySalaryValue(int id, int productId) throws PersonIdNotFoundException, ProductIdNotFoundException;

 

学习体会

通过本单元的学习,我初步理解了契约式编程。契约式编程的核心思想是对软件系统中的元素(如类、方法)之间相互合作以及“责任”与“权利”的比喻。使用契约式编程,可以提高程序的鲁棒性,便于测试,便于组织模块间通信与多人协作。

以此类推,契约式思想还可以应用到我们的生活中。在多人合作的任务中,我们也可以采用契约的思想,使用更加规格化的语言来厘清每个人的责任与权利,提高合作的效率。

关于单元作业,本单元作业相较于前两个单元轻松了很多,顶层架构都已经由规格规定,我们要做的就是忠实地实现规格的要求。需要注意的是,规格并没有规定我们实现的具体方法,因此需要我们自己根据时空复杂度限制来设计合适的实现方法。

关于单元实验,本单元的实验我认为比作业更有价值,在实验中介绍了java垃圾回收的机制,考察了我们在短时间内阅读规格并实现代码、阅读代码并归纳出规格的能力。

关于建议,我认为本单元作业对JML的考察要求其实不高,根据讨论区和身边的同学的情况来看,大家似乎更多地把注意力放到算法和数据结构上了,并没有怎么关心JML本身。所以感觉可以适当增加三单元实验课的次数,增加在有限的时间内独立写JML的机会。

最后,感谢老师和助教的辛苦付出,感谢在对拍中给予支持的小伙伴,感谢往届学长学姐不吝赐教分享博客。

posted @ 2022-06-03 22:09  和平鸽5106  阅读(213)  评论(0编辑  收藏  举报