BUAA_OO_2022 第三单元总结
BUAA_OO_2022 第三单元总结
O.前言
本单元主要内容是学习理解JML语言,在此规格的限制下进行迭代开发,完成对特定社交网络的简单模拟。
一、架构与性能分析
1.1 UML类图
对于阅读jml开发,个人理解通俗来说就是”看注释“写代码,因此在这三次作业中,其实架构大致已经被助教们设计好了,我们要完成的就是填充细节,因此在这里就只放上最后一次作业的UML类图。
1.2 hw9
其实最一开始接触到jml,我个人来讲是比较懵的,但是在通读一遍所给的文档后发现,这个单元的作业还是相对简单的,我们要做的主要是将jml翻译成现成的java代码,并保证一定的性能。(虽然说作业简单了,但是个人感觉实验就相对难了不少,毕竟作业更多的是在阅读理解jml,而实验就是要写一些jml,尤其是关于二叉树的一些jml描述,着实令人有些头疼不已)
而在第一次作业中,涉及到性能问题的主要指令就是:
- qci:查询两个结点是否未于同一个连通分量(两个人是否存在联系)
- qbs:查询图中有多少个连通分量(大致意思就是查询有多少个小圈子吧🤔)
毫无疑问,如果使用比较暴力的dfs进行深度搜索,qci、qbs都很容易会超时,因此我使用了并查集来进行优化,维护各个人所在集合的信息,大致实现如下:
public class DisjointSet {
private HashMap<Integer, Integer> parents = new HashMap<>();
private int branchNum = 0;
public void insert(int id) {
parents.put(id, id);
branchNum++;
}
public int find(int id) {
int now = id;
int root;
int father;
while (true) {
father = parents.get(now);
if (father == now) {
root = father;
break;
}
now = father;
}
now = id;
while (true) {
father = parents.get(now);
if (father == now) {
break;
}
parents.replace(now, root);
now = father;
}
return root;
}
public void clear() {
parents.clear();
branchNum = 0;
}
...
}
每次插入节点,该节点的父节点都是自己。每次进行查找时,将进行路径压缩,将路径上所有节点的父节点都设置为根节点,以实现再次查询时速度的提升。
1.3 hw10
在本次作业中,涉及到了Group的相关操作,但是涉及到性能问题的主要还是MyNetWork中的相关方法,主要指令如下:
- qgvs:查询小组中每个人的SocialValue总和
- qgav:查询小组中年龄的方差
- qlc:查询某个人距离另一个人的最近”距离”
首先对于qgvs命令,如果按照很直接的思路,每次查询都要遍历一遍小组内的人,超时的可能性是非常大的,因此我们可以通过维护MyGroup中的valueSum这一成员变量,实现查询时O(1)的复杂度。
具体实现主要包括,Group内添加或删除Person时对ValueSum进行加减。比较容易忽略的是,在addRelation时,也可能会影响到valueSum,因此对于每一个Person要知道自己所在的Group,在每次addRelation时要进行进行维护,具体实现如下:
public void addValueSum(Person person) {
for (Integer id : groups.keySet()) {
MyGroup myGroup = (MyGroup) groups.get(id);
if (myGroup.hasPerson(person)) {
myGroup.addValueSum(queryValue(person) * 2);
}
}
}
而对于qgav命令,做一个很简单的数学推导把平方式展开就可以,具体实现就不再多做赘述。
对于qlc命令,涉及到了我们数据结构中学习到的Prim、kruskal算法,二者主要区别在于Prim利用节点构造最小生成树,而kruskal则利用边来进行构造。在实现的过程中,我使用了kruskal算法,并利用了之前使用的并查集DisJionSet来进行了优化。
private int kruskal() {
int foundNum = 0;
Edge e;
int point1;
int point2;
int sum = 0;
for (int i = 0; foundNum < (hasFound.size() - 1) && i < list.size(); i++) {
e = list.get(i);
point1 = e.getPoint1();
point2 = e.getPoint2();
if (disjointSet.find(point1) != disjointSet.find(point2)) {
foundNum++;
disjointSet.link(point1, point2);
sum += e.getWeight();
}
}
return sum;
}
1.4 hw11
在本次作业中,性能主要涉及的命令只有sim,大致实现就是利用Dijsktra + 堆优化。
但是我在实现时犯了一个较大错误就是我的关系图并没有维护,而且在实现kruskal算法时,我就新增了一个Map类,用来进行求解最小生成树,而在实现Dijsktra算法时,我又用了另一个DMap来实现(真是不知道当时怎么想的,服了)。并且由于我的DMap没有维护,每一次调用时,都要遍历NetWork中的所有Person重新建图,性能可想而知。
public class DMap {
private PriorityQueue<Node> distances = new PriorityQueue();
private HashMap<Edge, Integer> minDisSet = new HashMap<>();
private static final int MAX_DISTANCE = Integer.MAX_VALUE;
....
}
public class Map {
private LinkedList<Edge> list = new LinkedList<>();
private DisjointSet disjointSet = new DisjointSet();
private HashSet<Integer> hasFound = new HashSet<>();
....
}
回头看这两个图就像是都是匆匆忙忙建起,只是为了应付"一时之需"。个人认为改进成如下这样才可以更好。在每次添加或删除时及时维护,在进行求解最小生成树、最短路径时,部分深克隆建图。
public class Map {
private HashMap<Person, Hashset<Edge>> edges = new HashMap<>();
private HashMap<Person, Node> nodes = new HashMap<>();
private DisjoinSet set;
public addPerson(Person person) {
...
}
public delPerson(Person person) {
...
}
public link(Person p1, Person p2) {
...
}
public kruskal(Person from) {
...
}
public dijsktra(Person from, Person ) {
...
}
}
二、测试与bug分析
测试部分
1.随机生成数据进行与同学进行对拍,既可以减少因理解jml有误而导致的bug,又相对节省时间,群策群力。
2.根据jml规格和性能限制手捏一些极端数据,比如group.size() < 1111, 又或者大量输入qci、大量输入qlc等。
3.使用Junit进行单元测试(实话实说用的比较少)
bug
hw9:没有测出bug
hw10:因为queryReceivedMessages()实现打错了方法,返回了所有的messages
hw11:大规模重新建图 + 堆优化时使用PriorityQueue的方法太过丑陋被卡TLE了
总结
个人认为,多数人的bug(包括我自己)主要是出现在优化时,对某些中间变量的维护。最典型的就是qgvs创建中间变量,在addRelation时,忘记维护,或者没有维护好。同时阅读jml时粗心大意,也是容易犯的错误。
并且随机生成测试数据进行测试也有着一定弊端,比如有些时候数据量不够大,bug就难以显现(比如hw10中我的bug,由于随机性,一个人收到的Message并不足够多,这个bug就没显现出来,不过也主要是因为当时注意力主要集中在性能方面,没有做什么测试)
值得一提的是,在对拍的时候,我们三个人曾出现一模一样的bug(sim命令会先把Message发送出去,在判断返回值,导致返回-1时,Message也会被发送),幸亏发现的早,及时修改(不然就强寄了😕)
三、NetWork接口扩展
对于新增的Advertiser、Producer、Customer三类建模:
- Advertiser:增加salesVolume字段以表示销售额,增加Producer字段表示合作的生产商,productType表示持有的商品的类型,productNum字段表示持有商品的数目
- Producer:增加productNum字段表示生产的商品的数目,productType表示生产的商品的类型
- Customer:增加preferenceType字段表示对商品的喜爱偏好,并增加subscription字段表示关注的订阅商,containsNum表示已购买的商品数量。
上述三者均继承于Person
而对于Advertisement继承于Message,其中新增advertiserId表示投放广告商的ID,customerId表示投放目标的ID
核心业务规格
sendAdvertisement:发送广告,顾客根据商品类型选择是否关注
/*@ public normal_behavior
@ requires containMessage(messageId) && getMessage(messageId) instance of Advertisment &&
@ containAdvertiser(getMessage(messageId).getAdvertiserId()) &&
@ containCustomer(getMessage(messageId).getCustomerId()) &&
@ getAdvertiser(getMessage(messageId).getAdvertiserId()).getProductType == getCustomer(getMessage(messageId).getCustomerId()).getPreferenceType &&
@ getCustomer(getMessage(messageId).getCustomerId()).isSubscribe() == false;
@ assignable getCustomer(getMessage(messageId).getCustomerId()).subscrition;
@ ensures getCustomer(getMessage(messageId).getCustomerId()).subscrition.size() == \old(getCustomer(getMessage(messageId).getCustomerId()).subscrition.size()) + 1;
@ ensures (\exists int i; 0 <= i && i <= getCustomer(getMessage(messageId).getCustomerId()).subscrition.size();
@ getCustomer(getMessage(messageId).getCustomerId()).subscrition[i] == getAdvertiser(getMessage(messageId).getAdvertiserId()));
@ ensures (\forall int i; 0 <= i && i <= \old(getCustomer(getMessage(messageId).getCustomerId()).subscrition.size());
@ (\exists int j; 0 <= j && j <= getCustomer(getMessage(messageId).getCustomerId()).subscrition.size());
@ \old(getCustomer(getMessage(messageId).getCustomerId()).subscrition)[i] == getCustomer(getMessage(messageId).getCustomerId()).subscrition[j]);
@ also
@ public normal_behavior
@ requires containMessage(messageId) && getMessage(messageId) instance of Advertisment &&
@ containAdvertiser(getMessage(messageId).getAdvertiserId()) &&
@ containCustomer(getMessage(messageId).getCustomerId()) &&
@ getAdvertiser(getMessage(messageId).getAdvertiserId()).getProductType == getCustomer(getMessage(messageId).getCustomerId()).getPreferenceType &&
@ getCustomer(getMessage(messageId).getCustomerId()).isSubscribe() == true;
@ assignable \nothing;
@ also
@ public normal_behavior
@ requires containMessage(messageId) && getMessage(messageId) instance of Advertisment &&
@ containAdvertiser(getMessage(messageId).getAdvertiserId()) &&
@ containCustomer(getMessage(messageId).getCustomerId()) &&
@ getAdvertiser(getMessage(messageId).getAdvertiserId()).getProductType != getCustomer(getMessage(messageId).getCustomerId()).getPreferenceType;
@ assignable \nothing;
@ also
@ public exceptional_behavior
@ signals (MessageIdNotFoundExceprion e) !containMessage(messageId) ||
@ !(getMessage(messageId) instance of Advertisment);
@ also
@ public exceptional_behavior
@ signals (AdvertiserIdNotFoundExceprion e) containMessage(messageId) ||
@ !(getMessage(messageId) instance of Advertisment) &&
@ !containAdvertiser(getMessage(messageId).getAdvertiserId());
@ also
@ public exeptional_behavior
@ signals (CustomerIdNotFoundExceprion e) containMessage(messageId) ||
@ !(getMessage(messageId) instance of Advertisment) &&
@ containAdvertiser(getMessage(messageId).getAdvertiserId()) &&
@ !containCustomer(getMessage(messageId).getCustomerId());
@*/
public void sendAdvertiserment(int id) throws MessageIdNotFoundExceprion,AdvertiserIdNotFoundExceprion,CustomerIdNotFoundExceprion;
purchase:表示购买意向,从广告商获得product
/*@ public normal_behavior
@ requires containAdvertiser(advertiserId) && containCustomer(customerId) && getAdvertiser(advertiserId).hasGoods() == true;
@ assignable getAdvertiser(advertiserId).saleVolume,getAdvertiser(advertiserId).productNum, getCustomer(customerId).containsNum, getCustomer(customerId).money;
@ ensures getAdvertiser(advertiserId).saleVolume == \old(getAdvertiser(advertiserId).saleVolume) + getAdvertiser(advertiserId).getProductPrice();
@ ensures getCustomer(customerId).containsNum == \old(getCustomer(customerId).containsNum) + 1;
@ ensures getCustomer(customerId).money == \old(getCustomer(customerId).money) - getAdvertiser(advertiserId).getProductPrice();
@ ensures getAdvertiser(advertiserId).productNum = \old(getAdvertiser(advertiserId).productNum) - 1;
@ also
@ public normal_behavior
@ requires containAdvertiser(advertiserId) && containCustomer(customerId) && getAdvertiser(advertiserId).hasGoods() == false;
@ assignable \nothing;
@ also
@ public exceptional_behavior
@ signals (AdvertiserIdNotFoundExceprion e) !containAdvertiser(advertiserId);
@ also
@ public exceptional_behavior
@ signals (CustomerIdNotFoundExceprion e) containAdvertiser(advertiserId) && !containCustomer(customerId);
@*/
public void purchase(int customerId, int advertiserId) throws AdvertiserIdNotFoundExceprion,CustomerIdNotFoundExceprion;
cooperate:达成合作后,生产商会给予广告商部分产品
/*@
@ public normal_behavior
@ requires containAdvertiser(advertiserId) && containProducer(producerId) && getAdvertiser(advertiser).isCooperate(producerId) == false;
@ assignable getAdvertiser(advertiseId).productNum, getProducer(producerId).productNum, getAdvertiser(advertiseId).producer;
@ ensures getAdvertiser(advertiseId).producer == getProducer(producerId);
@ ensures (\old(getProducer(producerId).productNum) > 5) ==>
(getProducer(producerId).productNum == \old(getProducer(producerId).productNum) - 5 &&getAdvertiser(advertiseId).productNum = 5);
@ ensures (\old(getProducer(producerId).productNum) <= 5) ==>
(getProducer(producerId).productNum == 0 && getAdvertiser(advertiseId).productNum == \old(getProducer(producerId).productNum));
@ also
@ public normal_behavior
@ requires containAdvertiser(advertiserId) && containProducer(producerId) && getAdvertiser(advertiser).isCooperate(producerId) == true;
@ also
@ public exceptional_behavior
@ signals (AdvertiserIdNotFoundExceprion e) !containAdvertiser(advertiserId);
@ also
@ public exceptional_behavior
@ signals (AdvertiserIdNotFoundExceprion e) containAdvertiser(advertiserId) && !contains(producerId);
@*/
public void cooperate(int producerId, int advertiserId) throws AdvertiserIdNotFoundExceprion,ProducerIdNotFoundExceprion;
四、思考与感想
- 通过本单元的学习,我认识并理解了JML。站在个人角度来看,他似乎更像是强化版的注释,可以帮助我们更精确的理解规格,极大程度上避免二义性产生。同时JML也可以更好的将任务划分,架构层次设计和具体实现可以在一定程度上剥离,更好地让我们投入到不同的任务当中去。
- 第三单元的任务对于前两个单元简单一些,但这也导致了我在学习的过程中,没花很多心思,犯了不少低级错误,态度不够认真,必须警醒。
- 总的来说JML是一个很强大的工具,但是网上的资料似乎并不多,要找一些东西更多需要去外网上查询,中文搜索大多数时候都会搜到往年学长学姐们的OO博客作业,比较希望OO课程组能提供一些课外阅读资料
(伸手党) - 本单元也让我复习了一些数据结构的知识,让我深刻意识到数据结构的重要性,以后有空还是要在好好深入学习数据结构,不然就真的成”调包侠“了。