BUAA-OO第三单元总结
总述
本单元的任务是实现简单的社交网络关系的模拟和查询, 包括人与人互动、消息收发等操作。学习目标是理解JML规格在面向对象设计与构造中的重要意义,并掌握利用JML规格提高代码质量的能力。官方包已经通过JML给定了整个社交网络的基本功能规格,如何设计层次之间的交互方法甚至额外层次是本单元作业的关键。
一、基于JML规格准备测试数据
JML为测试提供了划分依据和判定依据,代码需要实现的功能规格都已给出,可以针对每个具体方法构造单独构造测试数据。依据前置条件分别构造满足不同前置条件的数据类型,通过后置条件和不变式检查结果,以及异常行为是否正确。而用Junit可以实现测试执行的自动化。
仍然需要注意构造边界数据,比如 group 中人数上限是1111。针对age相关计算、不同类型消息的收发操作构造特殊数据。
由于部分操作需要遍历,如果没有没有采用合适的数据结构或算法,所有的查询和分析都要遍历整个大图,性能会极低,可以针对此类操作如qgav、 qbs、qlc等构造大量查询的数据。
二、架构设计与图模型构建和维护策略
2.1 第一次作业
-
query_circle query_block_sum
构架一个类 UnionFindSet 来维护并查集,采用按秩合并和路径压缩优化。person 的 id 为图中的节点,用 HashMap 维护父节点图和节点秩图,通过判断两点的顶级父节点是否相同来判断两点是否联系,同时在在增加节点和合并节点维护 blocknum。实现如下:
private final HashMap<Integer, Integer> parent; //key id - value parentId private final HashMap<Integer, Integer> rank; //key id - value rank private int blockNum; // add person public void insert(int id) { parent.put(id, id); rank.put(id, 1); blockNum = blockNum + 1; } // add relation public void union(int id1, int id2) { int root1 = find(id1); int root2 = find(id2); if (root1 == root2) { return; } if (rank.get(root1) < rank.get(root2)) { parent.put(root1, root2); } else if (rank.get(root1) > rank.get(root2)) { parent.put(root2, root1); } else { parent.put(root2, root1); int oldRank = rank.get(root1); rank.put(root1, oldRank + 1); } blockNum = blockNum - 1; } // isCircle public int find(int id) { if (parent.get(id) != id) { int newParent = find(parent.get(id)); parent.put(id, newParent); } return parent.get(id); }
2.2 第二次作业
-
query least connection
采用并查集优化 Kruskal 算法,在 NetWork 中的 unionFindSet 中重载 union 方法,用 HashMap<Integer, ArrayList
> 维护每个 block 中的边集合和边数,求最小生成树时获取当前查询节点的顶级父节点,进一步获取对应的 block 信息,然后传给 Kruskal 方法。在 Kruskal 算法中增加边时也用并查集来判断两节点的联通性。实现如下: 更新的 UnionFindSet:
private final HashMap<Integer, ArrayList<Edge>> edgeBlocks; // parentId - edgeBlock private final HashMap<Integer, HashSet<Integer>> personBlocks; // parentId - personBlock public void union(int id1, int id2, int value) { int root1 = find(id1); int root2 = find(id2); Edge edge = new Edge(id1, id2, value); ArrayList<Edge> edgeList1; ArrayList<Edge> edgeList2; if ((edgeList1 = edgeBlocks.get(root1)) == null) { edgeList1 = new ArrayList<>(); } if ((edgeList2 = edgeBlocks.get(root2)) == null) { edgeList2 = new ArrayList<>(); } HashSet<Integer> personSet1 = personBlocks.get(root1); HashSet<Integer> personSet2 = personBlocks.get(root2); if (root1 == root2) { edgeList1.add(edge); edgeBlocks.put(root1, edgeList1); personSet1.add(id2); personBlocks.put(root1, personSet1); return; } if (rank.get(root1) < rank.get(root2)) { parent.put(root1, root2); // root1'parent becomes root2 edgeList2.add(edge); edgeList2.addAll(edgeList1); edgeBlocks.put(root2, edgeList2); edgeBlocks.remove(root1); personSet2.addAll(personSet1); personBlocks.put(root2, personSet2); personBlocks.remove(root1); } else { parent.put(root2, root1); // root2'parent becomes root1 if (rank.get(root1).intValue() == rank.get(root2).intValue()) { int oldRank = rank.get(root1); rank.put(root1, oldRank + 1); } edgeList1.add(edge); edgeList1.addAll(edgeList2); edgeBlocks.put(root1, edgeList1); edgeBlocks.remove(root2); personSet1.addAll(personSet2); personBlocks.put(root1, personSet1); personBlocks.remove(root2); } blockNum = blockNum - 1; }
Kruskal:
public int kruskal(Network network, ArrayList<Edge> edges, int total) { int sum = 0; int num = 0; UnionFindSet ufs = new UnionFindSet(); edges.sort(Comparator.comparingInt(Edge::getWeight)); for (Edge edge : edges) { if (num >= (total - 1)) { return sum; } int id1 = edge.getId1(); int id2 = edge.getId2(); if (!ufs.contains(id1)) { ufs.insert(id1); } if (!ufs.contains(id2)) { ufs.insert(id2); } if (!ufs.isCircle(id1, id2)) { ufs.union(id1, id2); sum = sum + network.getPerson(id1).queryValue(network.getPerson(id2)); num = num + 1; } } return sum; }
2.3 第三次作业
-
注意区分什么时候用 message 的 id,什么时候用 emojiId。
-
send indirect message
采用堆优化的 Dijkstra 算法求最短路径,用优先队列 存放每个节点,需要注意 PriorityQueue 只有在增加或删除元素时才会重新排序,仅修改其中元素的值并不会触发排序。因为没有考虑到这一点强测 WA 了5个点。
public int dijkstra(Person src, Person dst) { PriorityQueue<MyPerson> pq = new PriorityQueue<>(Comparator.comparingInt(MyPerson::getPath)); ArrayList<Integer> over = new ArrayList<>(); HashSet<Integer> visit = new HashSet<>(); ((MyPerson) src).setPath(0); pq.add((MyPerson) src); visit.add(src.getId()); while (!pq.isEmpty()) { MyPerson p = pq.poll(); // 取出 删除 调整堆结构 over.add(p.getId()); // 已找到最短路径 if (p.getId() == dst.getId()) { return p.getPath(); } for (Person nowAcq : p.getAcquaintance().values()) { if (!over.contains(nowAcq.getId())) { MyPerson acq = (MyPerson) nowAcq; if (visit.contains(acq.getId())) { acq.setPath(Math.min(p.getPath() + p.queryValue(acq), acq.getPath())); } else { acq.setPath(p.getPath() + p.queryValue(acq)); visit.add(acq.getId()); } pq.remove(acq);//注意pq只修改元素但是不add 它不会重新排序!!!! pq.add(acq); // 加入堆尾 调整堆 //也可以直接add 在for之前用over加判断条件 } } } return -1; }
-
简便的删除操作
deleteColdEmoji
emojiIdHeat.values().removeIf(heat -> (heat < limit)); messages.values().removeIf(message -> ((message instanceof EmojiMessage) && (!containsEmojiId(((EmojiMessage)message).getEmojiId()))));
clearNotices
getPerson(personId).getMessages().removeIf(message -> (message instanceof NoticeMessage));
三、性能问题和修复情况
-
query_group_age_var
如果每次查询时都计算可能会超时。在group 中 add person、del person 时 维护 ageSum、ageSqrSum,那么在每次查询时就无序遍历。
ageMean = ageSum / people.size()
ageVar = (ageSqrSum - 2 * ageMean * ageSum + people.size() * ageMean * ageMean) / people.size()
需要注意如果直接由数学公式推导,ageSum 可以替换成 people.size() * ageMean,但这样会因为精度出现bug。
-
query_group_value_sum query_group_people_sum
与 age 操作类似,但除了在group 中 add person、del person 时只需要一层循环遍历维护 valueSum 外,add relation 时也需判断是否需要更新 group 的 valueSum。
-
算法的性能,采用路径压缩并查集和堆优化的Dijkstra。
四、NetWork 的扩展
4.1 要求
假设出现了几种不同的Person
- Advertiser:持续向外发送产品广告
- Producer:产品生产商,通过Advertiser来销售产品
- Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买 -- 所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
- Person:吃瓜群众,不发广告,不买东西,不卖东西
如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等 请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)。
4.2 规定
为了扩展NetWork,做出如下规定:
- 新增三个 Person 的子类为Advertiser、Producer、Customer,新增 Product 类,新增 ProductNotFoundException、EqualProductIdException、NotProducerException 异常类。
- Product 有 price 属性,有唯一的 id 属性。Advertiser 有一个 sales 属性,表示总销售额,有 experience 属性,表示经验值。
- 每个 Product 有一个唯一的id,Producer 可以生产 Product,之后可以向 Advertiser 发送ProductMessage,之后 Advertiser 可以向 Customer 推销产品,推销成功后,Advertiser 的经验值会加1。Customer 收到推销之后,只有 money 大于 Product 的 price 才会购买该 Product。
为满足新规定,Network 新增:
/*@ public instance model non_null Product[] products;
@*/
//@ ensures \result == (\exists int i; 0 <= i && i < products.length; products[i].getId() == id);
public /*@ pure @*/ boolean containsProduct(int id);
/*@ public normal_behavior
@ requires containsProduct(id);
@ ensures (\exists int i; 0 <= i && i < products.length; products[i].getId() == id &&
@ \result == products[i]);
@ also
@ public normal_behavior
@ requires !containsProduct(id);
@ ensures \result == null;
@*/
public /*@ pure @*/ Product getProduct(int id);
4.3 实现
-
功能1:生产商品
/*@ public normal_behavior @ requires !(\exists int i; 0 <= i && i < people.length; products[i].equals(product)); @ assignable products; @ assignable ((Producer)getPerson(product.getProducerId())).getProducts(); @ ensures products.length == \old(products.length) + 1; @ ensures (\forall int i; 0 <= i && i < \old(products.length); @ (\exists int j; 0 <= j && j < products.length; products[j].equals(\old(products[i])))); @ ensures (\exists int i; 0 <= i && i < products.length; products[i].equals(product); @ ensures ((Producer)getPerson(product.getProducerId)).getProductsNum() = \old(((Producer)getPerson(product.getProducerId())).getProductsNum()) + 1; @ ensures (\forall int i; 0 <= i && i < \old(((Producer)getPerson(product.getProducerId())).getProductsNum()); \exists int j; 0 <= j && j < ((Producer)getPerson(product.getProducerId())).getProductsNum(); ((Producer)getPerson(product.getProducerId())).getProducts().get(j).equals(\old(((Producer)getPerson(product.getProducerId())).getProducts().get(i)))); @ ensures (\exists int i; 0 <= i && i < ((Producer)getPerson(product.getProducerId())).getProductsNum(); ((Producer)getPerson(product.getProducerId())).getProducts().get(i).equals(product)); @ also @ public exceptional_behavior @ signals (EqualProductIdException e) containsProduct(product.getId()); @ signals (PersonIdNotFoundException e) (!containsProduct(product.getId()) && !contains(product.getProducerId())); @*/ public void produce(Product product) throws EqualProductIdException, PersonIdNotFoundException;
-
功能2:向顾客推销产品
/*@ public normal_behavior @ requires containsProduct(productId) && contains(adId) && contains(customerId); @ assignable getPerson(customerId).money, ((Producer)getPerson(getProduct(productId).getProducerId())).sales, ((Advertiser)getPerson(adId)).exp; @ ensures (getPerson(customerId).getMoney() >= getProduct(productId).getPrice()) ==> ((getPerson(customerId).getMoney() == \old(getPerson(customerId).getMoney()) - getProduct(productId).getPrice()) && (((Producer)getPerson(getProduct(productId).getProducerId())).getSales() = \old(((Producer)getPerson(getProduct(productId).getProducerId())).getSales()) + getProduct(productId).getPrice()) && (((Advertiser)getPerson(adId)).getExp() == \old(((Advertiser)getPerson(adId)).getExp()) + 1)); @ also @ public exceptional_behavior @ signals (ProductNotFoundException e) !containsProduct(productId); @ signals (PersonIdNotFoundException e) containsProduct(productId) && (!contains(adId) || !contains(customerId)); @*/ public void promote(int productId, int adId, int customerId) throws ProductNotFoundException, PersonIdNotFoundException;
-
功能3:查询某个 id 的 Producer 的销售额
/*@ public normal_behavior @ requires contains(id) && (getPerson(id) instanceof Producer); @ ensures /result == getPerson(id).getSales(); @ also @ public exceptional_behavior @ signals (PersonIdNotFoundException e) !contains(id); @ signals (NotProducerException e) (contains(id) && (!(getPerson(id) instanceof Producer))); @*/ public /*@ pure @*/ int querySales(int id) throws PersonIdNotFoundException, NotProducerException;
五、心得体会
总体来说,本单元作业没有前两个单元难度大。所有方法的具体操作都由JML明确规定,但同时需要我们严格遵守规格。jml 作为一种接口规格语言,可以准确定义和表示方法行为的正确性,提供了一种通过逻辑来检查代码的方式。
通过本单元的练习,我学会了jml的方法规格表示和基于规格的测试方法,以及类型层次下的规格分析设计,体会到了jml的优势。但在阅读一些比较复杂的jml描述时需要花很长时间理解,比如对qlc、qbs等方法的描述,面对复杂的jml时,可以通过画图、转换自然语言等加快理解。
此外,在作业中巩固了许多图论算法及其优化。