2022年北航OO第三单元总结

单元学习概要

本单元的主要学习内容是学习 JML 规格的相关知识,并根据给出的 JML 规格实现一个社交网络的模型,该模型中包括成员、群组以及相互之间的关系和流通的消息,并支持其中各种各样的功能和各种各样的异常。在完成实际规格要求的同时,还需要了解并使用图有关的相关算法,比如并查集、最小生成树算法和最短路径算法。

架构设计

由于 MyNetwork 具有全局唯一性,且所有的信息都在其中增添与流转,因此将所有信息,储存其中:

考虑到人员与群组 ID 的唯一性且其需要频繁查找,因此使用HashMap进行存储:

    private final HashMap<Integer, MyPerson> myPeople;
    private final HashMap<Integer, MyGroup> myGroups;

考虑到消息列表与表情列表的频繁删除性,而且 JAVA 中的容器没有完全实现的链表,因此自定义泛型链表 MyList<> 来进行对此两类数据的存储,同时考虑到其特性,在 MyList<> 周围又增添了其他成员,封装成完全的的个性化类:

    private final MessageList messages;
    private final EmojisList emojis;

考虑到算法本身是实现功能的方式,因此将所有算法相关操作进行解耦,完全粉装到特定类中去:

    private final AlgorithmCore algorithmCore;

其实,此类维护了 MyNetwork 中所有人员关系,即全局图。

其次,MyPerson 类与 Emoji 类中实现采取了细节上的封装:考虑到相识的人和相识值是成对出现的,表情 ID 列表与表情热度是一一对应的,因此再独立封装出两个类,分别为: emoji 与 AcqToValue;且两类都在 MyPerson 类构造并使用,实现对外隐藏;

异常处理方式就是简单地使用静态的 HashMap 进行处理即可。

AlgorithmCore 中对图的构建以及维护

自定义类 Net,实质上为一个最大连通子图,整个 AlgorithmCore 是一个单个元素为 Net 的 ArrayList。
在 Net 中,节点使用 整型 ArrayList 即可维护,而这里再自定义 Edge 类,当 MyNetwork 进行人员属性相关操作时,AlgorithmCore 捕获这些信息并同步化具体的图操作,以此,构建出与 MyNetwork 中一致的图。

性能问题及优化
  1. query_circle, query_block_sum
    这两条指令是第一次作业中最涉及到算法的部分,通过对整个框架的理解,query_circle 是查询两个结点是否处于同一个连通子图中,query_block_sum 是进行对全图中的最大连通子图数量。而 AlgorithmCore 中维护的 ArrayList 就是每个极大连图子图的 Net 的集合,因此只需要增添结点对 Net 的映射关系即可;

  2. query_least_connectio
    这条指令是在第二次作业中涉及到最小生成树的部分,我使用的是 Kruskal 算法来进行计算,通过手写链条实现加边时的边的排序处理,之后正常按照算法进行计算即可,为了避免连续重复的查询,因此额外设置脏位进行对性能的优化。

  3. query_group_value_sum
    这条指令是求出同一组内所有结点之间的边的权值和,为了防止数据过大导致该指令超时,在组中维护边集合,即在往组中添加和删除节点时,同时进行对其中相关联的边集合维护,同时修改其中记录的边权值总和。这样的处理,可以直接使该指令的复杂度为 O(1)。

  4. send_indirect_message
    第三次作业的这条指令要求我们实现对最短路径的查询。在使用 Dijkstra 算法的时候,注意到每次遍历未查询节点的时候可能会导致超时,因此这里处理是改善性能的关键。强测第六个点也是在卡是否进行了优化。

Bug分析

本单元作业唯一出现的一个 BUG 在于在计算群组年龄方差时由于将人员大小放于分母,导致第二次作业的三个点爆出除0异常。十分痛心!

测试数据

  1. 尝试使用 Junit 对代码进行了单元测试,尤其是上述提到的和算法有关的相关指令。并初步学习了解了 Junit 的使用。

  2. 自动化测试:通过大量随机生成与专门针对图的稠密度的数据,对程序进行攻击并检测 CPU 所用时间,同时使用 assert 写出评测机对运行结果的正确性进行校验。

Network 扩展

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

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

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

Customer:消费者,会关注广告并选择和自己偏好匹配的产品来购买

所谓购买,就是直接通过Advertiser给相应Producer发一个购买消息
Person:吃瓜群众,不发广告,不买东西,不卖东西

如此Network可以支持市场营销,并能查询某种商品的销售额和销售路径等

请讨论如何对Network扩展,给出相关接口方法,并选择3个核心业务功能的接口方法撰写JML规格(借鉴所总结的JML规格模式)

扩展
通过对需求的分析,Advertiser、Producer 和 Customer 可以继承 Person 的接口,Advertisement 和 BuyMessage 可以继承 Message 的接口。

发送广告:

/*@ public normal_behavior
  @ requires containsMessage(id) && (getMessage(id) instanceof Advertisement);
  @ assignable messages;
  @ assignable people[*].messages;
  @ 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 int i; 0 <= i && i < people.length && !getMessage(id).getPerson1().isLinked(people[i]);
  @           people[i].getMessages().equals(\old(people[i].getMessages()));
  @ ensures (\forall int i; 0 <= i && i < people.length && getMessage(id).getPerson1().isLinked(people[i]);
  @           (\forall int j; 0 <= j && j < \old(people[i].getMessages().size());
  @             people[i].getMessages().get(j+1) == \old(people[i].getMessages().get(j))) &&
  @           people[i].getMessages().get(0).equals(\old(getMessage(id))) &&
  @           people[i].getMessages().size() == \old(people[i].getMessages().size()) + 1);
  @ also   
  @ public exceptional_behavior  
  @ signals (MessageIdNotFoundException e) !containsMessage(id);  
  @ also  
  @ public exceptional_behavior  
  @ signals (AdvertisementIdNotFoundException e) containsMessage(id) && !  (getMessage(id) instanceof Advertisement);  
  @*/
public void sendAdvertisement(int id) throws
        MessageIdNotFoundException, AdvertisementIdNotFoundException;

生产商生产产品:

/*@ public normal_behavior
  @ requires contains(producerId) && (getPerson(producerId) instanceof Producer);
  @ assignable getProducer(producerId).productCount;
  @ ensures getProducer(producerId).getProductCount(productId) ==
  @           \old(getProducer(producerId).getProductCount(productId)) + 1;
  @ also
  @ public exceptional_behavior
  @ signals (PersonIdNotFoundException e) !contains(producerId);  
  @ also
  @ public exceptional_behavior  
  @ signals (ProducerIdNotFoundException e) contains(producerId) && !(getPerson(producerId) instanceof Producer);
  @*/
public void produceProduct(int producerId, int productId) throws
        PersonIdNotFoundException, ProducerIdNotFoundException;

发送购买消息:

/*@ public normal_behavior
  @ requires containsMessage(id) && (getMessage(id) instanceof BuyMessage);
  @ requires (getMessage(id).getPerson1() instanceof Customer) && (getMessage(id).getPerson2() instanceof Advertiser);
  @ assignable messages;
  @ assignable getMessage(id).getPerson1().money;
  @ assignable getMessage(id).getPerson2().messages, 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 (\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;
  @ ensures (\old(getMessage(id)).getPerson1().getMoney() ==
  @         \old(getMessage(id).getPerson1().getMoney()) - ((BuyMessage)\old(getMessage(id))).getMoney() &&
  @         \old(getMessage(id)).getPerson2().getMoney() ==
  @         \old(getMessage(id).getPerson2().getMoney()) + ((BuyMessage)\old(getMessage(id))).getMoney());
  @ also
  @ public exceptional_behavior
  @ signals (MessageIdNotFoundException e) !containsMessage(id);  
  @ also
  @ public exceptional_behavior
  @ signals (NotBuyMessageException e) containsMessage(id) && !(getMessage(id) instanceof BuyMessage);  
  @ also
  @ public exceptional_behavior  
  @ signals (NotCustomerException e) containsMessage(id) && (getMessage(id) instanceof BuyMessage) && !(getMessage(id).getPerson1() instanceof Customer);   
  @ also  
  @ public exceptional_behavior   
  @ signals (NotAdvertiserException e) (NotCustomerException e) containsMessage(id) && (getMessage(id) instanceof BuyMessage) && (getMessage(id).getPerson1() instanceof Customer) && !(getMessage(id).getPerson2() instanceof Advertiser);
  @*/
public void sendBuyMessage(int id) throws
        MessageIdNotFoundException, NotBuyMessageException, NotCustomerException, NotAdvertiserException;

学习体会

本单元的主要知识点就是 JML 规格,通过本单元的学习,对规格设计有了初步的认识,同时,也回顾了好几种和图有关的算法。
规格化设计的集成单元要求在代码书写过程中就可以在一定程度上保证代码的正确性,因此对 JML 的学习是十分有价值和必要的。
当然,在实现规格时,也要理解规格的本身逻辑,进而才能写出效率更高的代码。